Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
2321 lines
134 KiB
HTML
2321 lines
134 KiB
HTML
<!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">
|
||
<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); }
|
||
|
||
/* ── Zeitpicker ── */
|
||
.time-picker { display:flex; align-items:center; gap:0.4rem; flex-wrap:wrap; }
|
||
.tp-seg { display:flex; flex-direction:column; align-items:center; gap:0.15rem; }
|
||
.tp-seg-row { display:flex; align-items:center; gap:0.2rem; }
|
||
.tp-seg button {
|
||
width:24px; height:24px; background:var(--color-card);
|
||
border:1px solid var(--color-muted); border-radius:4px;
|
||
cursor:pointer; font-size:0.9rem; font-weight:700; color:var(--color-text);
|
||
display:flex; align-items:center; justify-content:center; padding:0; flex-shrink:0;
|
||
}
|
||
.tp-seg button:hover { background:var(--color-primary); color:#fff; border-color:var(--color-primary); }
|
||
.tp-seg input {
|
||
width:28px; text-align:center; background:var(--color-card);
|
||
border:1px solid var(--color-muted); border-radius:4px;
|
||
color:var(--color-text); font-size:0.9rem; font-weight:600;
|
||
font-family:monospace; padding:0.15rem 0; box-sizing:border-box;
|
||
}
|
||
.tp-seg .tp-label { font-size:0.62rem; color:var(--color-muted); text-transform:uppercase; letter-spacing:0.04em; }
|
||
.tp-colon { font-size:1rem; font-weight:700; color:var(--color-muted); margin-bottom:0.9rem; }
|
||
|
||
/* ── 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; }
|
||
|
||
/* ── 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; }
|
||
|
||
/* ── Spiel-Sets (gruppe-card style) ── */
|
||
.gs-card { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:10px; overflow:hidden; cursor:pointer; transition:border-color 0.15s; }
|
||
.gs-card:hover { border-color:var(--color-primary); }
|
||
.gs-card-header { display:flex; align-items:center; gap:0.75rem; padding:0.85rem 1rem; user-select:none; }
|
||
.gs-card-meta { flex:1; min-width:0; }
|
||
.gs-card-name { font-size:0.95rem; font-weight:600; color:var(--color-text); }
|
||
.gs-card-header-actions { display:flex; gap:0.4rem; flex-shrink:0; }
|
||
|
||
.gs-sub { margin-bottom:0.85rem; }
|
||
.gs-sub:last-child { margin-bottom:0; }
|
||
.gs-sub-header { display:flex; align-items:center; justify-content:space-between; margin-bottom:0.35rem; }
|
||
.gs-sub-title { font-size:0.7rem; font-weight:700; letter-spacing:0.06em; text-transform:uppercase; color:var(--color-primary); }
|
||
.gs-sub-warn { color:#e74c3c !important; }
|
||
|
||
.gs-item-list { display:flex; flex-direction:column; gap:0.25rem; }
|
||
.gs-list-item { border-radius:6px; background:var(--color-secondary); overflow:hidden; }
|
||
.gs-list-item-row { display:flex; align-items:center; gap:0.5rem; padding:0.3rem 0.5rem 0.3rem 0.75rem; cursor:pointer; user-select:none; transition:background 0.12s; }
|
||
.gs-list-item-row:hover { background:rgba(255,255,255,0.04); }
|
||
.gs-list-item.open .gs-list-item-row { background:rgba(233,69,96,0.08); }
|
||
.gs-list-item-text { color:var(--color-text); flex:1; min-width:0; font-size:0.83rem; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||
.gs-list-item-badges { display:flex; gap:0.3rem; flex-shrink:0; align-items:center; }
|
||
.gs-badge { font-size:0.68rem; padding:0.1rem 0.4rem; border-radius:20px; background:rgba(233,69,96,0.15); color:var(--color-primary); white-space:nowrap; }
|
||
.gs-badge-neutral { background:rgba(255,255,255,0.07); color:var(--color-muted); }
|
||
.gs-list-item-detail { display:none; padding:0.4rem 0.75rem 0.5rem; border-top:1px solid rgba(255,255,255,0.06); font-size:0.79rem; color:var(--color-muted); line-height:1.5; }
|
||
.gs-list-item.open .gs-list-item-detail { display:block; }
|
||
.gs-detail-text { color:var(--color-text); white-space:pre-wrap; margin-bottom:0.3rem; }
|
||
.gs-detail-actions { display:flex; gap:0.35rem; margin-top:0.4rem; justify-content:flex-end; }
|
||
.gs-btn-sub-add { background:none; border:1px solid var(--color-secondary); border-radius:5px; color:var(--color-muted); font-size:0.73rem; padding:0.15rem 0.5rem; cursor:pointer; transition:border-color 0.15s,color 0.15s; width:auto; }
|
||
.gs-btn-sub-add:hover { border-color:var(--color-primary); color:var(--color-primary); }
|
||
.gs-btn-item-edit { background:none; border:1px solid rgba(136,136,136,0.45); border-radius:5px; color:var(--color-muted); font-size:0.73rem; padding:0.18rem 0.55rem; cursor:pointer; transition:border-color 0.15s,color 0.15s; width:auto; }
|
||
.gs-btn-item-edit:hover { border-color:var(--color-text); color:var(--color-text); }
|
||
.gs-btn-item-delete { background:none; border:1px solid rgba(233,69,96,0.4); border-radius:5px; color:var(--color-primary); font-size:0.73rem; padding:0.18rem 0.55rem; cursor:pointer; transition:background 0.15s; width:auto; }
|
||
.gs-btn-item-delete:hover { background:rgba(233,69,96,0.15); }
|
||
.gs-sub-empty { font-size:0.78rem; color:var(--color-muted); padding:0.15rem 0; }
|
||
|
||
#gsSetModal, #gsItemModal { z-index:600; }
|
||
|
||
.gs-check-group { display:flex; flex-wrap:wrap; gap:0.4rem; }
|
||
.gs-check-chip { display:inline-flex; align-items:center; gap:0.4rem; background:var(--color-secondary); border:1px solid rgba(255,255,255,0.1); border-radius:20px; padding:0.25rem 0.7rem; cursor:pointer; font-size:0.82rem; color:var(--color-text); transition:border-color 0.15s; user-select:none; }
|
||
.gs-check-chip:has(input:checked) { border-color:var(--color-primary); }
|
||
.gs-check-chip input { accent-color:var(--color-primary); width:auto; cursor:pointer; margin:0; flex-shrink:0; }
|
||
</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>
|
||
|
||
<!-- Spiel-Sets (für Spiel-Karte) -->
|
||
<div style="display:flex;align-items:center;justify-content:space-between;margin:2rem 0 1rem;gap:1rem;flex-wrap:wrap;">
|
||
<div>
|
||
<h2 style="margin:0;">Spiel-Sets</h2>
|
||
<p style="margin:0.25rem 0 0;font-size:0.8rem;color:var(--color-muted);">Aufgaben-Sets für die Spiel-Karte im Karten-Lock · max. 5 Sets</p>
|
||
</div>
|
||
<button id="btnNewGameSet" onclick="openGsSetModal(null)" style="width:auto;padding:0.55rem 1.2rem;">+ Set anlegen</button>
|
||
</div>
|
||
<div class="template-list" id="gameSetList"></div>
|
||
<p id="gameSetEmpty" style="display:none;color:var(--color-muted);font-size:0.9rem;">Noch keine Spiel-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-colon">:</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">Std</span>
|
||
</div>
|
||
<div class="tp-colon">:</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">Min</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>
|
||
<!-- Spiel-Karte (CardLock) -->
|
||
<div class="form-section">
|
||
<div class="form-section-title">Spiel-Karte (optional)</div>
|
||
<div class="form-row" style="margin-bottom:0.5rem;">
|
||
<label>Spiel-Set</label>
|
||
<select id="fGameSetId" onchange="onGameSetChange()">
|
||
<option value="">Kein Spiel-Set</option>
|
||
</select>
|
||
</div>
|
||
<button class="btn-add" type="button" onclick="openGsSetModal(null,'template')">+ Neues Set anlegen</button>
|
||
<div id="gameSetSpieldauerRow" style="display:none;margin-top:0.75rem;">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:0.3rem;">
|
||
<label for="sldGameSpieldauer" style="font-size:0.88rem;">Spieldauer</label>
|
||
<span class="setting-value" id="valGameSpieldauer">Mittel</span>
|
||
</div>
|
||
<input type="range" id="sldGameSpieldauer" min="0" max="4" value="2" oninput="updateGameSpieldauer(this.value)" style="width:100%;">
|
||
</div>
|
||
</div>
|
||
<!-- Aufgaben (CardLock) -->
|
||
<div class="form-section">
|
||
<div class="form-section-title">Aufgaben (optional)</div>
|
||
<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</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>
|
||
</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-colon">:</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">Std</span></div>
|
||
<div class="tp-colon">:</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">Min</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-colon">:</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">Std</span></div>
|
||
<div class="tp-colon">:</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">Min</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-colon">:</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">Std</span></div>
|
||
<div class="tp-colon">:</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">Min</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-colon">:</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">Std</span></div>
|
||
<div class="tp-colon">:</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">Min</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-colon">:</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">Std</span></div>
|
||
<div class="tp-colon">:</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">Min</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-colon">:</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">Std</span></div>
|
||
<div class="tp-colon">:</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">Min</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-colon">:</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">Std</span></div>
|
||
<div class="tp-colon">:</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">Min</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>
|
||
|
||
<!-- Spiel-Set: Set erstellen / umbenennen -->
|
||
<div class="modal-backdrop" id="gsSetModal">
|
||
<div class="modal-box" style="max-width:420px;" onclick="event.stopPropagation()">
|
||
<h2 id="gsSetModalTitle" style="margin:0 0 1.25rem;color:var(--color-primary);">Neues Spiel-Set</h2>
|
||
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Name *</label>
|
||
<input type="text" id="gsSetName" maxlength="100" placeholder="Set-Name"
|
||
style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;transition:border-color 0.2s;"
|
||
onkeydown="if(event.key==='Enter')saveGsSet()">
|
||
<div id="gsSetError" style="color:#e74c3c;font-size:0.82rem;margin-top:0.75rem;display:none;"></div>
|
||
<div style="display:flex;justify-content:flex-end;gap:0.75rem;margin-top:1.5rem;">
|
||
<button onclick="closeGsSetModal()" style="background:var(--color-secondary);color:var(--color-text);border:none;border-radius:6px;padding:0.55rem 1.1rem;font-size:0.9rem;cursor:pointer;width:auto;">Abbrechen</button>
|
||
<button onclick="saveGsSet()" style="background:var(--color-primary);color:#fff;border:none;border-radius:6px;padding:0.55rem 1.1rem;font-size:0.9rem;font-weight:600;cursor:pointer;width:auto;">Speichern</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Spiel-Set: Item hinzufügen / bearbeiten -->
|
||
<div class="modal-backdrop" id="gsItemModal">
|
||
<div class="modal-box" style="max-width:460px;" onclick="event.stopPropagation()">
|
||
<h2 id="gsItemModalTitle" style="margin:0 0 1.25rem;color:var(--color-primary);">Aufgabe</h2>
|
||
|
||
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Titel *</label>
|
||
<input type="text" id="gsItemTitle" maxlength="150" placeholder="Titel …"
|
||
style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;margin-bottom:0.85rem;transition:border-color 0.2s;">
|
||
|
||
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Beschreibung (optional)</label>
|
||
<textarea id="gsItemDesc" rows="3" maxlength="600" placeholder="Beschreibung …"
|
||
style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.88rem;font-family:inherit;resize:vertical;outline:none;margin-bottom:0.85rem;"></textarea>
|
||
|
||
<!-- Aufgabe: Level + Dauer -->
|
||
<div id="gsItemAufgabeRow" style="display:none;margin-bottom:0.85rem;">
|
||
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Level *</label>
|
||
<select id="gsItemAufgabeLevel" style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;appearance:none;margin-bottom:0.65rem;">
|
||
<option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option>
|
||
</select>
|
||
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Dauer (Minuten, optional)</label>
|
||
<input type="number" id="gsItemMinutes" min="1" max="9999" placeholder="–"
|
||
style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;">
|
||
</div>
|
||
|
||
<!-- Aufgabe: benötigt -->
|
||
<div id="gsItemBenoetigtRow" style="display:none;margin-bottom:0.85rem;">
|
||
<div style="font-size:0.8rem;color:#aaa;margin-bottom:0.4rem;">Benötigt</div>
|
||
<div class="gs-check-group">
|
||
<label class="gs-check-chip"><input type="checkbox" id="gsItemBen_MUND">Mund</label>
|
||
<label class="gs-check-chip"><input type="checkbox" id="gsItemBen_ANUS">Anus</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Zeitstrafe: Level + Strafminuten -->
|
||
<div id="gsItemZeitstrafeRow" style="display:none;margin-bottom:0.85rem;">
|
||
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Level *</label>
|
||
<select id="gsItemZeitstrafeLevel" style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;appearance:none;margin-bottom:0.65rem;">
|
||
<option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option>
|
||
</select>
|
||
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Strafminuten (Von – Bis)</label>
|
||
<div style="display:flex;gap:0.6rem;align-items:center;">
|
||
<input type="number" id="gsItemMinMin" min="1" max="9999" placeholder="Min."
|
||
style="flex:1;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;">
|
||
<span style="color:var(--color-muted);">–</span>
|
||
<input type="number" id="gsItemMaxMin" min="1" max="9999" placeholder="Max."
|
||
style="flex:1;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;">
|
||
</div>
|
||
<label style="display:block;font-size:0.8rem;color:#aaa;margin:0.65rem 0 0.3rem;">Text bei Aufhebung (optional)</label>
|
||
<textarea id="gsItemReleaseText" rows="2" maxlength="2000"
|
||
placeholder="Text der angezeigt wird, wenn die Sperre endet…"
|
||
style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.88rem;font-family:inherit;resize:vertical;outline:none;transition:border-color 0.2s;line-height:1.45;"></textarea>
|
||
</div>
|
||
|
||
<!-- Zeitstrafe: sperrt -->
|
||
<div id="gsItemSperrtRow" style="display:none;margin-bottom:0.85rem;">
|
||
<div style="font-size:0.8rem;color:#aaa;margin-bottom:0.4rem;">Sperrt</div>
|
||
<div class="gs-check-group">
|
||
<label class="gs-check-chip"><input type="checkbox" id="gsItemSperr_MUND"> Mund</label>
|
||
<label class="gs-check-chip"><input type="checkbox" id="gsItemSperr_ANUS"> Anus</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Zeitstrafe + Finisher: Temp. Öffnung -->
|
||
<div id="gsItemUnlockRow" style="display:none;margin-bottom:0.85rem;">
|
||
<div style="font-size:0.8rem;color:#aaa;margin-bottom:0.4rem;">Temp. Öffnung</div>
|
||
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.88rem;margin-bottom:0.35rem;cursor:pointer;">
|
||
<input type="checkbox" id="gsItemBefore" style="accent-color:var(--color-primary);width:15px;height:15px;"> Vor der Maßnahme erforderlich
|
||
</label>
|
||
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.88rem;cursor:pointer;">
|
||
<input type="checkbox" id="gsItemAfter" style="accent-color:var(--color-primary);width:15px;height:15px;"> Nach der Maßnahme erforderlich
|
||
</label>
|
||
</div>
|
||
|
||
<div id="gsItemError" style="color:#e74c3c;font-size:0.82rem;display:none;margin-bottom:0.5rem;"></div>
|
||
<div style="display:flex;justify-content:flex-end;gap:0.75rem;margin-top:0.5rem;">
|
||
<button onclick="closeGsItemModal()" style="background:var(--color-secondary);color:var(--color-text);border:none;border-radius:6px;padding:0.55rem 1.1rem;font-size:0.9rem;cursor:pointer;width:auto;">Abbrechen</button>
|
||
<button onclick="saveGsItem()" style="background:var(--color-primary);color:#fff;border:none;border-radius:6px;padding:0.55rem 1.1rem;font-size:0.9rem;font-weight:600;cursor:pointer;width:auto;">Speichern</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Spiel-Set: Inhalt-Popup -->
|
||
<div class="modal-backdrop" id="gsEditModal">
|
||
<div class="modal-box" style="max-width:600px;width:calc(100% - 2rem);" onclick="event.stopPropagation()">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;gap:0.75rem;margin-bottom:1.25rem;">
|
||
<h2 id="gsEditModalTitle" style="margin:0;color:var(--color-primary);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"></h2>
|
||
<button onclick="closeGsEditModal()" style="background:none;border:none;color:var(--color-muted);font-size:1.3rem;line-height:1;cursor:pointer;padding:0;width:auto;flex-shrink:0;">✕</button>
|
||
</div>
|
||
<div id="gsEditModalContent"></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>
|
||
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;
|
||
|
||
// ── Zeitpicker ──
|
||
function tpChange(prefix, delta, seg) {
|
||
let d = parseInt(document.getElementById(prefix + '_d').value) || 0;
|
||
let h = parseInt(document.getElementById(prefix + '_h').value) || 0;
|
||
let m = parseInt(document.getElementById(prefix + '_m').value) || 0;
|
||
if (seg === 'm') m += delta;
|
||
else if (seg === 'h') h += delta;
|
||
else d += delta;
|
||
if (m >= 60) { h += Math.floor(m/60); m %= 60; }
|
||
if (m < 0) { const b = Math.ceil(-m/60); h -= b; m += b*60; }
|
||
if (h >= 24) { d += Math.floor(h/24); h %= 24; }
|
||
if (h < 0) { const b = Math.ceil(-h/24); d -= b; h += b*24; }
|
||
if (d < 0) d = 0;
|
||
document.getElementById(prefix + '_d').value = d;
|
||
document.getElementById(prefix + '_h').value = String(h).padStart(2,'0');
|
||
document.getElementById(prefix + '_m').value = String(m).padStart(2,'0');
|
||
}
|
||
function tpToMinutes(prefix) {
|
||
const d = parseInt(document.getElementById(prefix + '_d').value) || 0;
|
||
const h = parseInt(document.getElementById(prefix + '_h').value) || 0;
|
||
const m = parseInt(document.getElementById(prefix + '_m').value) || 0;
|
||
return d*1440 + h*60 + m;
|
||
}
|
||
function tpFromMinutes(prefix, total) {
|
||
total = total || 0;
|
||
const d = Math.floor(total/1440), h = Math.floor((total%1440)/60), m = total%60;
|
||
document.getElementById(prefix + '_d').value = d;
|
||
document.getElementById(prefix + '_h').value = String(h).padStart(2,'0');
|
||
document.getElementById(prefix + '_m').value = String(m).padStart(2,'0');
|
||
}
|
||
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; }
|
||
}
|
||
|
||
// ── 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-colon">:</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">Std</span></div>
|
||
<div class="tp-colon">:</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">Min</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, '"');
|
||
const descVal = (data?.description || '').replace(/</g, '<').replace(/>/g, '>');
|
||
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();
|
||
}
|
||
|
||
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 = '<option value="">Kein Spiel-Set</option>';
|
||
_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 = '<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, 720) + '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;
|
||
}
|
||
|
||
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');
|
||
|
||
// 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');
|
||
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('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(); });
|
||
|
||
// ── 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('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; }
|
||
|
||
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: document.getElementById('fGameSetId').value || null,
|
||
gameSpieldauerIdx: parseInt(document.getElementById('sldGameSpieldauer').value) || 2,
|
||
};
|
||
} 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();
|
||
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();
|
||
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 `<span class="${cls}">L${i+1}: ${c}</span>`;
|
||
}).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 = `
|
||
<div class="gs-card-header">
|
||
<div class="gs-card-meta">
|
||
<div class="gs-card-name">${esc(s.name)}</div>
|
||
<div style="display:flex;gap:0.3rem;flex-wrap:wrap;margin-top:0.3rem;">
|
||
${lvlBadges}
|
||
<span class="gs-badge gs-badge-neutral">Zeitstrafen: ${zeitstrafen.length}</span>
|
||
<span class="${finBadgeCls}">Finisher: ${finisher.length}</span>
|
||
</div>
|
||
</div>
|
||
<div class="gs-card-header-actions" onclick="event.stopPropagation()">
|
||
<button class="gs-btn-item-edit" onclick="openGsSetModal('${s.id}')">✎</button>
|
||
<button class="gs-btn-item-delete" onclick="deleteGameSet('${s.id}',${JSON.stringify(s.name)})">✕</button>
|
||
</div>
|
||
</div>`;
|
||
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('') ||
|
||
'<div class="gs-sub-empty">–</div>';
|
||
html += `<div class="gs-sub">
|
||
<div class="gs-sub-header">
|
||
<span class="gs-sub-title${warnCls}">Level ${l} <span style="font-weight:400;">(${items.length}/3+)</span></span>
|
||
<button class="gs-btn-sub-add" onclick="openGsItemModal('aufgabe','${s.id}',null,${l})">+ Aufgabe</button>
|
||
</div>
|
||
<div class="gs-item-list">${itemsHtml}</div></div>`;
|
||
}
|
||
const zeitHtml = zeitstrafen.map((z, i) => gsZeitstrafeRowHtml(s.id, i, z)).join('') ||
|
||
'<div class="gs-sub-empty">–</div>';
|
||
html += `<div class="gs-sub">
|
||
<div class="gs-sub-header">
|
||
<span class="gs-sub-title">Zeitstrafen <span style="font-weight:400;">(${zeitstrafen.length})</span></span>
|
||
<button class="gs-btn-sub-add" onclick="openGsItemModal('zeitstrafe','${s.id}',null,null)">+ Zeitstrafe</button>
|
||
</div>
|
||
<div class="gs-item-list">${zeitHtml}</div></div>`;
|
||
const finWarnCls = finisher.length < 1 ? ' gs-sub-warn' : '';
|
||
const finHtml = finisher.map((f, i) => gsFinisherRowHtml(s.id, i, f)).join('') ||
|
||
'<div class="gs-sub-empty">–</div>';
|
||
html += `<div class="gs-sub">
|
||
<div class="gs-sub-header">
|
||
<span class="gs-sub-title${finWarnCls}">Finisher <span style="font-weight:400;">(${finisher.length}/1+)</span></span>
|
||
<button class="gs-btn-sub-add" onclick="openGsItemModal('finisher','${s.id}',null,null)">+ Finisher</button>
|
||
</div>
|
||
<div class="gs-item-list">${finHtml}</div></div>`;
|
||
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 ? `<span class="gs-badge gs-badge-neutral">${a.minutes} Min.</span>` : '',
|
||
...toolLabels.map(l => `<span class="gs-badge gs-badge-neutral">${l}</span>`),
|
||
].join('');
|
||
const desc = a.description ? `<div class="gs-detail-text">${esc(a.description)}</div>` : '';
|
||
return `<div class="gs-list-item" id="gsli_${setId}_a_${gi}">
|
||
<div class="gs-list-item-row" onclick="toggleGsListItem('gsli_${setId}_a_${gi}')">
|
||
<span class="gs-list-item-text">${esc(a.title)}</span>
|
||
<div class="gs-list-item-badges">${badges}</div>
|
||
</div>
|
||
<div class="gs-list-item-detail">${desc}
|
||
<div class="gs-detail-actions">
|
||
<button class="gs-btn-item-edit" onclick="openGsItemModal('aufgabe','${setId}',${gi},null)">✎ Bearbeiten</button>
|
||
<button class="gs-btn-item-edit" onclick="duplicateGsItem('aufgabe','${setId}',${gi})">⧉ Kopie</button>
|
||
<button class="gs-btn-item-delete" onclick="deleteGsItem('aufgabe','${setId}',${gi})">✕ Löschen</button>
|
||
</div>
|
||
</div></div>`;
|
||
}
|
||
|
||
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 ? `<span class="gs-badge">L${z.level}</span>` : '',
|
||
`<span class="gs-badge gs-badge-neutral">${timeStr}</span>`,
|
||
...sperrtLabels.map(l => `<span class="gs-badge gs-badge-neutral">🔒 ${l}</span>`),
|
||
z.releaseText ? `<span class="gs-badge gs-badge-neutral">📝 Aufhebung</span>` : '',
|
||
z.tempUnlockBeforeRequired ? `<span class="gs-badge gs-badge-neutral">🔓 Vorher</span>` : '',
|
||
z.tempUnlockAfterRequired ? `<span class="gs-badge gs-badge-neutral">🔓 Nachher</span>` : '',
|
||
].join('');
|
||
const releaseRow = z.releaseText ? `<div style="font-size:0.75rem;color:var(--color-muted);margin-bottom:0.15rem;">Bei Aufhebung:</div><div class="gs-detail-text">${esc(z.releaseText)}</div>` : '';
|
||
const desc = z.description ? `<div class="gs-detail-text">${esc(z.description)}</div>` : '';
|
||
return `<div class="gs-list-item" id="gsli_${setId}_z_${idx}">
|
||
<div class="gs-list-item-row" onclick="toggleGsListItem('gsli_${setId}_z_${idx}')">
|
||
<span class="gs-list-item-text">${esc(z.title)}</span>
|
||
<div class="gs-list-item-badges">${badges}</div>
|
||
</div>
|
||
<div class="gs-list-item-detail">${desc}${releaseRow}
|
||
<div class="gs-detail-actions">
|
||
<button class="gs-btn-item-edit" onclick="openGsItemModal('zeitstrafe','${setId}',${idx},null)">✎ Bearbeiten</button>
|
||
<button class="gs-btn-item-edit" onclick="duplicateGsItem('zeitstrafe','${setId}',${idx})">⧉ Kopie</button>
|
||
<button class="gs-btn-item-delete" onclick="deleteGsItem('zeitstrafe','${setId}',${idx})">✕ Löschen</button>
|
||
</div>
|
||
</div></div>`;
|
||
}
|
||
|
||
function gsFinisherRowHtml(setId, idx, f) {
|
||
const badges = [
|
||
f.tempUnlockBeforeRequired ? `<span class="gs-badge gs-badge-neutral">🔓 Vorher</span>` : '',
|
||
f.tempUnlockAfterRequired ? `<span class="gs-badge gs-badge-neutral">🔓 Nachher</span>` : '',
|
||
].join('');
|
||
const desc = f.description ? `<div class="gs-detail-text">${esc(f.description)}</div>` : '';
|
||
return `<div class="gs-list-item" id="gsli_${setId}_f_${idx}">
|
||
<div class="gs-list-item-row" onclick="toggleGsListItem('gsli_${setId}_f_${idx}')">
|
||
<span class="gs-list-item-text">${esc(f.title)}</span>
|
||
<div class="gs-list-item-badges">${badges}</div>
|
||
</div>
|
||
<div class="gs-list-item-detail">${desc}
|
||
<div class="gs-detail-actions">
|
||
<button class="gs-btn-item-edit" onclick="openGsItemModal('finisher','${setId}',${idx},null)">✎ Bearbeiten</button>
|
||
<button class="gs-btn-item-edit" onclick="duplicateGsItem('finisher','${setId}',${idx})">⧉ Kopie</button>
|
||
<button class="gs-btn-item-delete" onclick="deleteGsItem('finisher','${setId}',${idx})">✕ Löschen</button>
|
||
</div>
|
||
</div></div>`;
|
||
}
|
||
|
||
// ── 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(); });
|
||
</script>
|
||
</body>
|
||
</html>
|