Files
xxx-sphere-web/bin/main/static/games/chastity/keyholder.html
Mario 2b0ce62d33
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
Menp überarbeitet
2026-04-08 16:52:43 +02:00

1924 lines
105 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Keyholder xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.lock-list { display:flex; flex-direction:column; gap:0.5rem; margin-top:0.5rem; }
/* Karte = Container */
.lock-card {
background:var(--color-card);
border:1px solid var(--color-secondary);
border-radius:10px;
overflow:hidden;
transition:border-color 0.15s;
}
.lock-card.open { border-color:var(--color-primary); }
/* Header-Zeile (klickbar) */
.lock-card-header {
display:flex; align-items:center; gap:0.9rem;
padding:0.75rem 1rem;
cursor:pointer; user-select:none;
}
.lock-card-header:hover { background:rgba(255,255,255,0.03); }
.lock-card-avatar {
width:52px; height:52px;
border-radius:50%;
background:var(--color-secondary);
display:flex; align-items:center; justify-content:center;
font-size:1.4rem; flex-shrink:0; overflow:hidden;
border:1px solid rgba(255,255,255,0.08);
}
.lock-card-avatar img { width:100%; height:100%; object-fit:cover; }
.lock-card-body { flex:1; min-width:0; display:flex; flex-direction:column; gap:0.15rem; }
.lock-card-line1 { font-size:0.78rem; color:var(--color-muted); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.lock-card-line2 { font-weight:700; font-size:0.95rem; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.lock-card-line3 { font-size:0.78rem; color:var(--color-muted); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.lock-card-actions { display:flex; gap:0.5rem; flex-shrink:0; }
.lock-card-actions button, .lock-card-actions a.btn {
margin-top:0; padding:0.3rem 0.7rem; font-size:0.8rem; width:auto;
}
.lock-toggle { font-size:0.75rem; color:var(--color-muted); flex-shrink:0; transition:transform 0.2s; }
.lock-card.open .lock-toggle { transform:rotate(90deg); }
/* Ausgeklappter Bereich */
.lock-detail-body {
display:none;
border-top:1px solid var(--color-secondary);
padding:1rem 1rem 0.85rem;
}
.lock-card.open .lock-detail-body { display:block; }
.empty-hint { color:var(--color-muted); font-size:0.9rem; margin-top:0.5rem; }
.detail-section { margin-bottom:1.1rem; }
.detail-section-title {
font-size:0.72rem; font-weight:700; color:var(--color-primary);
text-transform:uppercase; letter-spacing:0.06em; margin-bottom:0.4rem;
}
.detail-row { display:flex; justify-content:space-between; align-items:baseline; gap:0.5rem; padding:0.2rem 0; }
.detail-label { font-size:0.85rem; color:var(--color-muted); }
.detail-value { font-size:0.9rem; font-weight:600; text-align:right; }
.detail-value.ok { color:var(--color-success); }
.detail-value.warn { color:#e67e22; }
.detail-value.danger { color:var(--color-primary); }
.violation-item {
font-size:0.82rem; color:var(--color-muted);
padding:0.25rem 0; border-bottom:1px solid var(--color-secondary);
}
.violation-item:last-child { border-bottom:none; }
/* Zeitpicker (für Freeze-Dialog) */
.time-picker { display:flex; align-items:center; gap:0.5rem; }
.tp-seg { display:flex; flex-direction:column; align-items:center; gap:0.2rem; }
.tp-seg-row { display:flex; align-items:center; gap:0.25rem; }
.tp-seg button {
width:26px; height:26px;
background:var(--color-secondary); border:1px solid var(--color-muted);
border-radius:5px; cursor:pointer; font-size:1rem; 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:30px; text-align:center;
background:var(--color-secondary); border:1px solid var(--color-muted);
border-radius:4px; color:var(--color-text);
font-size:0.95rem; font-weight:600; font-family:monospace;
padding:0.18rem 0; box-sizing:border-box;
}
.tp-seg .tp-label { font-size:0.65rem; color:var(--color-muted); text-transform:uppercase; letter-spacing:0.04em; }
.tp-colon { font-size:1.1rem; font-weight:700; color:var(--color-muted); margin-bottom:1rem; }
/* ── Tab-Navigation ── */
.kh-tabs { display:flex; gap:0; border-bottom:2px solid var(--color-secondary); margin-bottom:1.25rem; }
.kh-tab {
padding:0.55rem 1.1rem; font-size:0.9rem; font-weight:600; cursor:pointer;
border:none; background:none; color:var(--color-muted);
border-bottom:2px solid transparent; margin-bottom:-2px;
transition:color 0.15s, border-color 0.15s;
}
.kh-tab.active { color:var(--color-text); border-bottom-color:var(--color-primary); }
/* ── Vorlagen-Typ-Icon (wie in meine-locks.html) ── */
.template-type-icon {
position:relative; width:2.2rem; height:2.2rem; flex-shrink:0;
display:flex; align-items:center; justify-content:center;
}
.template-type-icon .icon-base { font-size:1.8rem; line-height:1; }
.template-type-icon img.icon-base { width:1.8rem; height:1.8rem; object-fit:contain; }
.template-type-icon .icon-lock {
position:absolute; bottom:-2px; right:-4px;
font-size:1.5rem; line-height:1;
filter: drop-shadow(0 0 2px rgba(0,0,0,0.5));
}
/* ── Angebote-Tab ── */
.offer-card {
background:var(--color-card); border:1px solid var(--color-secondary);
border-radius:10px; padding:0.85rem 1rem; margin-bottom:0.6rem;
display:flex; align-items:center; gap:0.85rem;
}
.offer-card-body { flex:1; min-width:0; }
.offer-card-name { font-weight:700; font-size:0.95rem; margin-bottom:0.2rem;
white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.offer-card-meta { font-size:0.78rem; color:var(--color-muted); display:flex; flex-wrap:wrap; gap:0.4rem; }
.offer-badge {
display:inline-block; font-size:0.72rem; padding:0.1rem 0.45rem;
border-radius:4px; background:rgba(255,255,255,0.07); border:1px solid var(--color-secondary);
}
.offer-badge.direct { background:rgba(46,204,113,0.12); border-color:rgba(46,204,113,0.3); color:#2ecc71; }
.offer-badge.confirm { background:rgba(230,126,34,0.12); border-color:rgba(230,126,34,0.3); color:#e67e22; }
.btn-offer-del {
background:rgba(231,76,60,0.1); border:1px solid rgba(231,76,60,0.3); color:#e74c3c;
border-radius:7px; padding:0.3rem 0.65rem; font-size:0.8rem; cursor:pointer; flex-shrink:0; width:auto;
}
/* Combobox (Vorlage-Auswahl im Angebot-Modal) */
.combo-wrap { position: relative; }
.combo-wrap input[type="text"] { width: 100%; box-sizing: border-box; }
.combo-dropdown {
display: none; position: absolute; top: calc(100% + 3px);
left: 0; right: 0; background: var(--color-card);
border: 1px solid var(--color-secondary); border-radius: 8px;
max-height: 220px; overflow-y: auto; z-index: 600;
box-shadow: 0 4px 16px rgba(0,0,0,0.25);
}
.combo-dropdown.open { display: block; }
.combo-option {
padding: 0.55rem 0.85rem; cursor: pointer;
font-size: 0.9rem; color: var(--color-text);
}
.combo-option:hover { background: var(--color-secondary); }
.combo-empty { padding: 0.55rem 0.85rem; font-size: 0.85rem; color: var(--color-muted); font-style: italic; }
</style>
</head>
<body class="app">
<div class="main">
<div class="content">
<h1 style="margin-bottom:1rem;">Keyholder</h1>
<!-- Tab-Navigation -->
<div class="kh-tabs">
<button class="kh-tab active" onclick="switchTab('lockees')">Meine Lockees</button>
<button class="kh-tab" onclick="switchTab('offers')">Keyholder-Angebote</button>
</div>
<!-- Tab: Meine Lockees -->
<div id="tabLockees">
<div class="lock-list" id="locksGrid"></div>
<p class="empty-hint" id="locksEmpty" style="display:none;">Du bist aktuell bei keinem Lock als Keyholder eingetragen.</p>
</div>
<!-- Tab: Keyholder-Angebote -->
<div id="tabOffers" style="display:none;">
<div style="display:flex;justify-content:flex-end;margin-bottom:1rem;">
<button style="width:auto;padding:0.5rem 1.1rem;" onclick="openCreateOfferModal()">+ Angebot erstellen</button>
</div>
<div id="offersList"></div>
<p class="empty-hint" id="offersEmpty" style="display:none;">Du hast noch keine Keyholder-Angebote erstellt.</p>
<p id="offersLimitHint" style="display:none;font-size:0.82rem;color:var(--color-muted);margin-top:0.5rem;"></p>
</div>
</div>
</div>
<!-- Angebot-Erstellen-Modal -->
<div id="createOfferModal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.65);z-index:500;align-items:center;justify-content:center;">
<div style="background:var(--color-card);border:1px solid var(--color-secondary);border-radius:14px;padding:1.5rem;max-width:440px;width:92%;max-height:88vh;overflow-y:auto;display:flex;flex-direction:column;gap:1rem;position:relative;">
<button onclick="closeCreateOfferModal()" style="position:absolute;top:0.75rem;right:0.75rem;background:none;border:none;color:var(--color-muted);font-size:1.3rem;cursor:pointer;padding:0.2rem 0.5rem;line-height:1;"></button>
<h3 id="offerModalTitle" style="margin:0;font-size:1.05rem;">🔑 Keyholder-Angebot erstellen</h3>
<!-- Template-Auswahl -->
<div>
<div style="font-size:0.72rem;font-weight:700;color:var(--color-primary);text-transform:uppercase;letter-spacing:0.06em;margin-bottom:0.4rem;">Vorlage</div>
<div class="combo-wrap" id="offerTemplateCombo">
<input type="text" id="offerTemplateInput" placeholder="Vorlage suchen…" autocomplete="off"
style="padding:0.5rem 0.75rem;border-radius:7px;border:1px solid var(--color-secondary);background:var(--color-secondary);color:var(--color-text);font-size:0.88rem;">
<div class="combo-dropdown" id="offerTemplateDropdown"></div>
<input type="hidden" id="offerTemplateValue">
</div>
</div>
<!-- Ziel-Geschlechter -->
<div>
<div style="font-size:0.72rem;font-weight:700;color:var(--color-primary);text-transform:uppercase;letter-spacing:0.06em;margin-bottom:0.5rem;">Richtet sich an</div>
<div style="display:flex;flex-direction:column;gap:0.35rem;">
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.88rem;cursor:pointer;">
<input type="checkbox" id="offerGenderAll" onchange="toggleAllGenders(this)" style="width:auto;"> Alle Geschlechter
</label>
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.88rem;cursor:pointer;">
<input type="checkbox" name="offerGender" value="WEIBLICH" style="width:auto;"> Weiblich
</label>
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.88rem;cursor:pointer;">
<input type="checkbox" name="offerGender" value="MAENNLICH" style="width:auto;"> Männlich
</label>
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.88rem;cursor:pointer;">
<input type="checkbox" name="offerGender" value="DIVERS" style="width:auto;"> Divers
</label>
</div>
</div>
<!-- Startmodus -->
<div>
<div style="font-size:0.72rem;font-weight:700;color:var(--color-primary);text-transform:uppercase;letter-spacing:0.06em;margin-bottom:0.5rem;">Startmodus</div>
<div style="display:flex;flex-direction:column;gap:0.35rem;">
<label style="display:flex;align-items:flex-start;gap:0.5rem;font-size:0.88rem;cursor:pointer;line-height:1.4;">
<input type="radio" name="offerStartMode" value="direct" checked style="width:auto;margin-top:2px;">
<span><strong>Direktstart</strong> Lock startet sofort, du wirst als Keyholder eingetragen</span>
</label>
<label style="display:flex;align-items:flex-start;gap:0.5rem;font-size:0.88rem;cursor:pointer;line-height:1.4;">
<input type="radio" name="offerStartMode" value="confirm" style="width:auto;margin-top:2px;">
<span><strong>Mit Bestätigung</strong> du erhältst eine Einladung und kannst annehmen oder ablehnen</span>
</label>
</div>
</div>
<div id="createOfferError" style="display:none;font-size:0.85rem;color:#e74c3c;"></div>
<div style="display:flex;gap:0.6rem;justify-content:flex-end;margin-top:0.25rem;">
<button onclick="closeCreateOfferModal()" style="background:none;border:1px solid var(--color-secondary);color:var(--color-muted);padding:0.5rem 1.1rem;border-radius:7px;cursor:pointer;font-size:0.88rem;width:auto;">Abbrechen</button>
<button id="createOfferSubmitBtn" onclick="submitCreateOffer()" style="padding:0.5rem 1.25rem;border-radius:7px;font-size:0.88rem;font-weight:600;width:auto;" id="createOfferSubmitBtn">Erstellen</button>
</div>
</div>
</div>
<!-- Verifikations-Prüfen-Modal -->
<div id="verificationVoteModal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.65);z-index:500;align-items:center;justify-content:center;">
<div style="background:var(--color-card);border:1px solid var(--color-secondary);border-radius:14px;padding:1.5rem;max-width:420px;width:90%;max-height:85vh;overflow-y:auto;display:flex;flex-direction:column;gap:1rem;">
<h3 style="margin:0;font-size:1.05rem;">Verifikation prüfen</h3>
<img id="verificationVoteImg" src="" alt="Verifikationsbild" style="width:100%;border-radius:8px;display:block;">
<div style="display:flex;gap:0.5rem;">
<button id="verificationVoteBtnDown" onclick="submitVerificationVote(false)"
style="flex:1;background:rgba(231,76,60,0.12);border:1px solid rgba(231,76,60,0.35);color:#e74c3c;padding:0.55rem 0;border-radius:7px;cursor:pointer;font-size:0.9rem;font-weight:600;width:auto;">
👎 Ablehnen
</button>
<button id="verificationVoteBtnUp" onclick="submitVerificationVote(true)"
style="flex:1;background:rgba(46,204,113,0.15);border:1px solid rgba(46,204,113,0.4);color:#2ecc71;padding:0.55rem 0;border-radius:7px;cursor:pointer;font-size:0.9rem;font-weight:600;width:auto;">
👍 Bestätigen
</button>
</div>
<button onclick="closeVerificationModal()"
style="background:none;border:1px solid var(--color-secondary);color:var(--color-muted);padding:0.45rem 1rem;border-radius:7px;cursor:pointer;font-size:0.88rem;width:auto;align-self:flex-end;">
Abbrechen
</button>
</div>
</div>
<!-- Karten-Bearbeiten-Modal -->
<div id="cardEditModal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.65);z-index:500;align-items:center;justify-content:center;">
<div style="background:var(--color-card);border:1px solid var(--color-secondary);border-radius:14px;padding:1.5rem;max-width:400px;width:90%;max-height:85vh;overflow-y:auto;display:flex;flex-direction:column;gap:1rem;">
<h3 id="cardEditTitle" style="margin:0;font-size:1.05rem;"></h3>
<div id="cardEditInputs" style="display:flex;flex-direction:column;gap:0.35rem;"></div>
<label style="display:flex;align-items:flex-start;gap:0.6rem;font-size:0.85rem;color:var(--color-muted);cursor:pointer;line-height:1.5;">
<input type="checkbox" id="cardEditNotifyDetailed" checked style="margin-top:3px;width:auto;flex-shrink:0;">
<span>Detailierte Beschreibung der Änderung mitteilen</span>
</label>
<div id="cardEditError" style="display:none;font-size:0.85rem;color:#e74c3c;"></div>
<div style="display:flex;gap:0.6rem;justify-content:flex-end;margin-top:0.25rem;">
<button onclick="closeCardModal()" style="background:none;border:1px solid var(--color-secondary);color:var(--color-muted);padding:0.5rem 1.1rem;border-radius:7px;cursor:pointer;font-size:0.88rem;width:auto;">Abbrechen</button>
<button id="cardEditSubmit" onclick="submitCardEdit()" style="background:var(--color-primary);border:none;color:#fff;padding:0.5rem 1.1rem;border-radius:7px;cursor:pointer;font-size:0.88rem;font-weight:600;width:auto;">Bestätigen</button>
</div>
</div>
</div>
<!-- Aufgabe-stellen-Dialog -->
<div id="assignTaskModal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.65);z-index:500;align-items:center;justify-content:center;">
<div style="background:var(--color-card);border:1px solid var(--color-secondary);border-radius:14px;padding:1.5rem;max-width:460px;width:92%;max-height:88vh;overflow-y:auto;display:flex;flex-direction:column;gap:1rem;position:relative;">
<button onclick="closeAssignTaskModal()" style="position:absolute;top:0.75rem;right:0.75rem;background:none;border:none;color:var(--color-muted);font-size:1.3rem;cursor:pointer;padding:0.2rem 0.5rem;line-height:1;"></button>
<h3 style="margin:0;font-size:1.05rem;">✅ Aufgabe stellen</h3>
<div>
<div style="font-size:0.72rem;font-weight:700;color:var(--color-primary);text-transform:uppercase;letter-spacing:0.06em;margin-bottom:0.4rem;">Aufgabe auswählen</div>
<div style="position:relative;">
<input type="text" id="assignTaskInput" placeholder="Aufgabe suchen…" autocomplete="off"
style="width:100%;box-sizing:border-box;background:var(--color-secondary);border:1px solid var(--color-muted);border-radius:7px;padding:0.5rem 0.75rem;color:var(--color-text);font-size:0.88rem;outline:none;">
<div id="assignTaskDropdown" style="display:none;position:absolute;top:calc(100% + 3px);left:0;right:0;background:var(--color-card);border:1px solid var(--color-secondary);border-radius:8px;max-height:200px;overflow-y:auto;z-index:600;box-shadow:0 4px 16px rgba(0,0,0,0.25);"></div>
<input type="hidden" id="assignTaskHiddenIdx" value="">
</div>
</div>
<div>
<div style="font-size:0.72rem;font-weight:700;color:var(--color-primary);text-transform:uppercase;letter-spacing:0.06em;margin-bottom:0.4rem;">Annahme-Frist</div>
<div class="time-picker">
<div class="tp-seg">
<div class="tp-seg-row">
<button type="button" onclick="atTpChange(-1,'d')"></button>
<input type="text" id="at_d" value="0" readonly>
<button type="button" onclick="atTpChange(1,'d')">+</button>
</div>
<span class="tp-label">Tage</span>
</div>
<span class="tp-colon">:</span>
<div class="tp-seg">
<div class="tp-seg-row">
<button type="button" onclick="atTpChange(-1,'h')"></button>
<input type="text" id="at_h" value="01" readonly>
<button type="button" onclick="atTpChange(1,'h')">+</button>
</div>
<span class="tp-label">Stunden</span>
</div>
<span class="tp-colon">:</span>
<div class="tp-seg">
<div class="tp-seg-row">
<button type="button" onclick="atTpChange(-1,'m')"></button>
<input type="text" id="at_m" value="00" readonly>
<button type="button" onclick="atTpChange(1,'m')">+</button>
</div>
<span class="tp-label">Minuten</span>
</div>
</div>
</div>
<div>
<div style="font-size:0.72rem;font-weight:700;color:var(--color-primary);text-transform:uppercase;letter-spacing:0.06em;margin-bottom:0.4rem;">Strafe bei Ablehnung / Ablauf</div>
<div style="display:flex;flex-direction:column;gap:0.5rem;">
<label style="display:flex;align-items:center;gap:0.6rem;font-size:0.88rem;cursor:pointer;">
<input type="checkbox" id="atPenaltyFreezeEnabled" onchange="toggleAtFreezeFields()" style="width:auto;flex-shrink:0;">
<span>Einfrieren für …</span>
</label>
<div id="atPenaltyFreezeFields" style="padding-left:1.6rem;opacity:0.4;pointer-events:none;transition:opacity 0.15s;">
<div class="time-picker">
<div class="tp-seg">
<div class="tp-seg-row">
<button type="button" onclick="atFreezeTpChange(-1,'d')"></button>
<input type="text" id="atf_d" value="0" readonly>
<button type="button" onclick="atFreezeTpChange(1,'d')">+</button>
</div>
<span class="tp-label">Tage</span>
</div>
<span class="tp-colon">:</span>
<div class="tp-seg">
<div class="tp-seg-row">
<button type="button" onclick="atFreezeTpChange(-1,'h')"></button>
<input type="text" id="atf_h" value="04" readonly>
<button type="button" onclick="atFreezeTpChange(1,'h')">+</button>
</div>
<span class="tp-label">Stunden</span>
</div>
<span class="tp-colon">:</span>
<div class="tp-seg">
<div class="tp-seg-row">
<button type="button" onclick="atFreezeTpChange(-1,'m')"></button>
<input type="text" id="atf_m" value="00" readonly>
<button type="button" onclick="atFreezeTpChange(1,'m')">+</button>
</div>
<span class="tp-label">Minuten</span>
</div>
</div>
</div>
<label style="display:flex;align-items:center;gap:0.6rem;font-size:0.88rem;cursor:pointer;">
<input type="checkbox" id="atPenaltyRedEnabled" onchange="toggleAtRedFields()" style="width:auto;flex-shrink:0;">
<span>Rote Karten hinzufügen:</span>
</label>
<div id="atPenaltyRedFields" style="padding-left:1.6rem;display:flex;align-items:center;gap:0.5rem;opacity:0.4;pointer-events:none;transition:opacity 0.15s;">
<button type="button" onclick="atRedChange(-1)" style="width:26px;height:26px;background:var(--color-secondary);border:1px solid var(--color-muted);border-radius:5px;cursor:pointer;font-size:1rem;font-weight:700;color:var(--color-text);display:flex;align-items:center;justify-content:center;padding:0;"></button>
<input type="text" id="atRedCount" value="1" readonly style="width:30px;text-align:center;background:var(--color-secondary);border:1px solid var(--color-muted);border-radius:4px;color:var(--color-text);font-size:0.95rem;font-weight:600;font-family:monospace;padding:0.18rem 0;">
<button type="button" onclick="atRedChange(1)" style="width:26px;height:26px;background:var(--color-secondary);border:1px solid var(--color-muted);border-radius:5px;cursor:pointer;font-size:1rem;font-weight:700;color:var(--color-text);display:flex;align-items:center;justify-content:center;padding:0;">+</button>
<span style="font-size:0.82rem;color:var(--color-muted);">rote Karten</span>
</div>
</div>
</div>
<div id="assignTaskError" style="display:none;font-size:0.85rem;color:#e74c3c;"></div>
<div style="display:flex;gap:0.6rem;justify-content:flex-end;">
<button onclick="closeAssignTaskModal()" style="background:none;border:1px solid var(--color-secondary);color:var(--color-muted);padding:0.5rem 1.1rem;border-radius:7px;cursor:pointer;font-size:0.88rem;width:auto;">Abbrechen</button>
<button onclick="submitAssignTask()" style="background:var(--color-primary);border:none;color:#fff;padding:0.5rem 1.1rem;border-radius:7px;cursor:pointer;font-size:0.88rem;font-weight:600;width:auto;">✅ Aufgabe stellen</button>
</div>
</div>
</div>
<!-- Freeze-Dialog -->
<div id="freezeModal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.65);z-index:500;align-items:center;justify-content:center;">
<div style="background:var(--color-card);border:1px solid var(--color-secondary);border-radius:14px;padding:1.5rem;max-width:380px;width:90%;display:flex;flex-direction:column;gap:1rem;position:relative;">
<button onclick="closeFreezeModal()" style="position:absolute;top:0.75rem;right:0.75rem;background:none;border:none;color:var(--color-muted);font-size:1.3rem;cursor:pointer;padding:0.2rem 0.5rem;line-height:1;"></button>
<h3 style="margin:0;font-size:1.05rem;">❄️ Lock einfrieren</h3>
<p style="margin:0;font-size:0.85rem;color:var(--color-muted);line-height:1.5;">
Für wie lange soll das Lock eingefroren werden?<br>
<strong style="color:var(--color-text);">Hinweis:</strong> Während des Einfrierens können keine weiteren Karten gezogen werden.
</p>
<div class="time-picker">
<div class="tp-seg">
<div class="tp-seg-row">
<button type="button" onclick="freezeTpChange(-1,'d')"></button>
<input type="text" id="freeze_d" value="0" readonly>
<button type="button" onclick="freezeTpChange(1,'d')">+</button>
</div>
<span class="tp-label">Tage</span>
</div>
<span class="tp-colon">:</span>
<div class="tp-seg">
<div class="tp-seg-row">
<button type="button" onclick="freezeTpChange(-1,'h')"></button>
<input type="text" id="freeze_h" value="04" readonly>
<button type="button" onclick="freezeTpChange(1,'h')">+</button>
</div>
<span class="tp-label">Stunden</span>
</div>
<span class="tp-colon">:</span>
<div class="tp-seg">
<div class="tp-seg-row">
<button type="button" onclick="freezeTpChange(-1,'m')"></button>
<input type="text" id="freeze_m" value="00" readonly>
<button type="button" onclick="freezeTpChange(1,'m')">+</button>
</div>
<span class="tp-label">Minuten</span>
</div>
</div>
<div id="freezeModalError" style="display:none;font-size:0.85rem;color:#e74c3c;"></div>
<div style="display:flex;gap:0.6rem;justify-content:flex-end;">
<button onclick="closeFreezeModal()" style="background:none;border:1px solid var(--color-secondary);color:var(--color-muted);padding:0.5rem 1.1rem;border-radius:7px;cursor:pointer;font-size:0.88rem;width:auto;">Abbrechen</button>
<button onclick="submitFreeze()" style="background:rgba(52,152,219,0.15);border:1px solid rgba(52,152,219,0.4);color:#3498db;padding:0.5rem 1.1rem;border-radius:7px;cursor:pointer;font-size:0.88rem;font-weight:600;width:auto;">❄️ Einfrieren</button>
</div>
</div>
</div>
<!-- Unfreeze-Bestätigungs-Dialog -->
<div id="unfreezeModal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.65);z-index:500;align-items:center;justify-content:center;">
<div style="background:var(--color-card);border:1px solid var(--color-secondary);border-radius:14px;padding:1.5rem;max-width:360px;width:90%;display:flex;flex-direction:column;gap:1rem;position:relative;">
<button onclick="closeUnfreezeModal()" style="position:absolute;top:0.75rem;right:0.75rem;background:none;border:none;color:var(--color-muted);font-size:1.3rem;cursor:pointer;padding:0.2rem 0.5rem;line-height:1;"></button>
<h3 style="margin:0;font-size:1.05rem;">Lock entfrieren</h3>
<p style="margin:0;font-size:0.85rem;color:var(--color-muted);line-height:1.5;">
Soll das Lock wirklich vorzeitig entfroren werden? Der Lockee kann dann wieder Karten ziehen.
</p>
<div style="display:flex;gap:0.6rem;justify-content:flex-end;">
<button onclick="closeUnfreezeModal()" style="background:none;border:1px solid var(--color-secondary);color:var(--color-muted);padding:0.5rem 1.1rem;border-radius:7px;cursor:pointer;font-size:0.88rem;width:auto;">Abbrechen</button>
<button onclick="submitUnfreeze()" style="background:rgba(52,152,219,0.15);border:1px solid rgba(52,152,219,0.4);color:#3498db;padding:0.5rem 1.1rem;border-radius:7px;cursor:pointer;font-size:0.88rem;font-weight:600;width:auto;">Entfrieren</button>
</div>
</div>
</div>
<script src="/js/card-defs.js"></script>
<script src="/js/card-display.js"></script>
<script src="/js/icons.js"></script>
<script src="/js/nav.js"></script>
<script src="/js/social-sidebar.js"></script>
<script>
function esc(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }
const lockDetailCache = {};
const lockTypeMap = {}; // lockId → 'CARDLOCK' | 'TIMELOCK'
// ── Meine Locks als Keyholder ──
async function loadLocks() {
try {
const [clRes, tlRes] = await Promise.all([
fetch('/keyholder/as-keyholder'),
fetch('/keyholder/timelock/as-keyholder')
]);
const cardLocks = clRes.ok ? await clRes.json() : [];
const timeLocks = tlRes.ok ? await tlRes.json() : [];
const locks = [
...cardLocks.map(l => ({ ...l, lockType: 'CARDLOCK' })),
...timeLocks.map(l => ({ ...l, lockType: 'TIMELOCK' }))
];
locks.forEach(l => { lockTypeMap[l.lockId] = l.lockType; });
const grid = document.getElementById('locksGrid');
grid.innerHTML = '';
const empty = document.getElementById('locksEmpty');
if (locks.length === 0) { empty.style.display = ''; return; }
empty.style.display = 'none';
locks.forEach(l => {
const av = l.lockeeProfilePic
? `<div class="lock-card-avatar"><img src="data:image/jpeg;base64,${l.lockeeProfilePic}" alt=""></div>`
: `<div class="lock-card-avatar">👤</div>`;
const startDate = l.startTime ? new Date(l.startTime).toLocaleDateString('de-DE') : '';
const frozenBadge = (l.isFrozenByKeyholder || l.isFrozen) ? ' · ❄️ Eingefroren' : '';
const emergencyBadge = l.emergencyUnlockRequested ? ' · 🆘 Notfall!' : '';
const line3 = l.lockType === 'TIMELOCK'
? `⏱ TimeLock · seit ${startDate}${frozenBadge}${emergencyBadge}`
: `🃏 ${l.totalCards} Karten · seit ${startDate}${frozenBadge}${emergencyBadge}`;
const card = document.createElement('div');
card.className = 'lock-card';
card.dataset.lockId = l.lockId;
card.innerHTML = `
<div class="lock-card-header">
${av}
<div class="lock-card-body">
<div class="lock-card-line1">${esc(l.lockeeName)}</div>
<div class="lock-card-line2">${esc(l.lockName)}</div>
<div class="lock-card-line3">${line3}</div>
</div>
<span class="lock-toggle">▶</span>
</div>
<div class="lock-detail-body">Wird geladen…</div>`;
card.querySelector('.lock-card-header').addEventListener('click', () => toggleLock(card, l.lockId));
grid.appendChild(card);
});
} catch(e) { console.error(e); }
}
async function toggleLock(card, lockId) {
const isOpen = card.classList.contains('open');
// Alle anderen schließen
document.querySelectorAll('#locksGrid .lock-card.open').forEach(c => c.classList.remove('open'));
if (isOpen) return;
card.classList.add('open');
await reloadLockDetail(lockId);
}
async function reloadLockDetail(lockId) {
const card = document.querySelector(`[data-lock-id="${lockId}"]`);
const body = card ? card.querySelector('.lock-detail-body') : null;
try {
const endpoint = lockTypeMap[lockId] === 'TIMELOCK'
? '/keyholder/timelock/as-keyholder/' + lockId
: '/keyholder/as-keyholder/' + lockId;
const res = await fetch(endpoint);
if (!res.ok) { if (body) body.textContent = 'Fehler beim Laden.'; return; }
const d = await res.json();
lockTypeMap[lockId] = d.lockType || lockTypeMap[lockId] || 'CARDLOCK';
lockDetailCache[lockId] = d;
if (body) {
body.innerHTML = buildDetailHtml(d);
body.dataset.loaded = '1';
attachDetailListeners(body, lockId);
}
// Listenkarte line3 aktualisieren
const line3 = card ? card.querySelector('.lock-card-line3') : null;
if (line3) {
const startDate = d.startTime ? new Date(d.startTime).toLocaleDateString('de-DE') : '';
const frozenBadge = (d.isFrozenByKeyholder || d.isFrozen) ? ' · ❄️ Eingefroren' : '';
if (d.lockType === 'TIMELOCK') {
line3.textContent = `⏱ TimeLock · seit ${startDate}${frozenBadge}`;
} else {
line3.textContent = `🃏 ${d.totalCards} Karten · seit ${startDate}${frozenBadge}`;
}
}
} catch(e) { if (body) body.textContent = 'Fehler beim Laden.'; }
}
function attachDetailListeners(body, lockId) {
const pruefenLink = body.querySelector('.prufen-link');
if (pruefenLink) {
pruefenLink.addEventListener('click', e => {
e.preventDefault();
openVerificationModal(lockId);
});
}
}
function buildTimeLockDetailHtml(d) {
let html = `<div style="margin-bottom:0.75rem;">
<a href="/community/benutzer.html?userId=${d.lockeeId}" style="font-size:0.82rem;color:var(--color-primary);">Profil ansehen →</a>
</div>`;
// Zeitinfo
html += `<div class="detail-section"><div class="detail-section-title">TimeLock</div>`;
if (d.timeUp) {
html += `<div class="detail-row"><span class="detail-label">Status</span><span class="detail-value ok">✅ Entsperrbereit</span></div>`;
} else if (d.unlockTime) {
html += `<div class="detail-row">
<span class="detail-label">Entsperrt um</span>
<span class="detail-value">${new Date(d.unlockTime).toLocaleString('de-DE')}</span>
</div>`;
} else {
html += `<div class="detail-row"><span class="detail-label">Entsperrzeit</span><span class="detail-value" style="color:var(--color-muted);">Nicht sichtbar</span></div>`;
}
if (d.isFrozen) {
const fu = d.frozenUntil ? new Date(d.frozenUntil).toLocaleString('de-DE') : 'unlimitiert';
html += `<div class="detail-row"><span class="detail-label">❄️ Eingefroren bis</span><span class="detail-value danger">${fu}</span></div>`;
}
html += `</div>`;
// Verifikation
if (d.requiresVerification) {
html += `<div class="detail-section"><div class="detail-section-title">Verifikation heute</div>`;
if (!d.verificationDoneToday) {
html += `<div class="detail-row"><span class="detail-label">Status</span><span class="detail-value danger">Warte auf Verifikation</span></div>`;
} else if (!d.verificationMyVote) {
html += `<div class="detail-row">
<span class="detail-label">Status</span>
<span class="detail-value warn">Ausstehend &nbsp;<a href="#" class="prufen-link" style="font-size:0.82rem;color:var(--color-primary);font-weight:600;">Prüfen →</a></span>
</div>`;
} else if (d.verificationMyVote === 'upvote') {
html += `<div class="detail-row"><span class="detail-label">Status</span><span class="detail-value ok">✓ Erledigt</span></div>`;
} else {
html += `<div class="detail-row"><span class="detail-label">Status</span><span class="detail-value danger">✗ Abgelehnt</span></div>`;
}
html += `</div>`;
}
// Einfrieren
html += `<div class="detail-section"><div class="detail-section-title">Einfrieren</div>`;
if (d.isFrozen) {
const fu = d.frozenUntil ? new Date(d.frozenUntil).toLocaleString('de-DE') : 'unbegrenzt';
html += `<div class="detail-row">
<span class="detail-label">❄️ Eingefroren bis</span>
<span class="detail-value danger">${fu}</span>
</div>
<div style="margin-top:0.5rem;">
<button onclick="openUnfreezeModal('${d.lockId}')" style="background:rgba(52,152,219,0.15);border:1px solid rgba(52,152,219,0.4);color:#3498db;padding:0.4rem 0.9rem;border-radius:7px;cursor:pointer;font-size:0.82rem;font-weight:600;width:auto;">❄️ Entfrieren</button>
</div>`;
} else {
html += `<div style="margin-top:0.25rem;">
<button onclick="openFreezeModal('${d.lockId}')" style="background:rgba(52,152,219,0.15);border:1px solid rgba(52,152,219,0.4);color:#3498db;padding:0.4rem 0.9rem;border-radius:7px;cursor:pointer;font-size:0.82rem;font-weight:600;width:auto;">❄️ Einfrieren</button>
</div>`;
}
html += `</div>`;
// Gestartet am
if (d.startTime) {
html += `<div style="font-size:0.78rem;color:var(--color-muted);margin-top:0.5rem;">
Gestartet am ${new Date(d.startTime).toLocaleDateString('de-DE')}
</div>`;
}
// Notfall-Banner
if (d.emergencyUnlockRequested && !d.keyholderRequestedUnlock) {
html += `<div style="display:flex;align-items:flex-start;gap:0.75rem;background:rgba(231,76,60,0.12);border:2px solid rgba(231,76,60,0.5);border-radius:10px;padding:1rem 1.1rem;margin-top:0.75rem;margin-bottom:0.5rem;">
<span style="font-size:1.4rem;flex-shrink:0;">🆘</span>
<div>
<div style="font-weight:700;font-size:0.95rem;color:#e74c3c;margin-bottom:0.25rem;">Notfall-Entsperrung angefordert!</div>
<div style="font-size:0.85rem;color:var(--color-muted);line-height:1.5;">Deine Lockee bittet dringend um Freigabe des Locks. Reagiere innerhalb einer Stunde oder das Lock öffnet sich automatisch.</div>
<div style="margin-top:0.6rem;">
<button onclick="requestUnlock('${d.lockId}')"
style="background:rgba(46,204,113,0.2);border:1px solid rgba(46,204,113,0.5);color:#2ecc71;padding:0.4rem 0.9rem;border-radius:7px;cursor:pointer;font-size:0.85rem;font-weight:600;width:auto;">
🔓 Jetzt freigeben
</button>
</div>
</div>
</div>`;
}
// Lock entsperren
html += `<div class="detail-section"><div class="detail-section-title">Lock entsperren</div>`;
if (d.keyholderRequestedUnlock) {
html += `<div style="font-size:0.85rem;color:var(--color-muted);">✅ Unlock wurde angefordert die Lockee erhält beim nächsten Laden ihren Entsperrcode.</div>`;
} else {
html += `<div id="unlockConfirm_${d.lockId}">
<button onclick="showUnlockConfirm('${d.lockId}')"
style="background:rgba(46,204,113,0.15);border:1px solid rgba(46,204,113,0.4);color:#2ecc71;padding:0.4rem 0.9rem;border-radius:7px;cursor:pointer;font-size:0.82rem;font-weight:600;width:auto;">
🔓 Lock freigeben
</button>
</div>`;
}
html += `</div>`;
return html;
}
function buildDetailHtml(d) {
if (d.lockType === 'TIMELOCK') return buildTimeLockDetailHtml(d);
let html = `<div style="margin-bottom:0.75rem;">
<a href="/community/benutzer.html?userId=${d.lockeeId}" style="font-size:0.82rem;color:var(--color-primary);">Profil ansehen →</a>
</div>`;
// Karten
html += `<div class="detail-section">
<div class="detail-section-title">Verbleibende Karten (${d.totalCards})</div>`;
html += cardTypeGridHtml(d.cardCounts || {});
html += `<div style="display:flex;gap:0.5rem;flex-wrap:wrap;margin-top:0.75rem;">
<button onclick="openCardModal('${d.lockId}', 'add')" style="background:rgba(46,204,113,0.15);border:1px solid rgba(46,204,113,0.4);color:#2ecc71;padding:0.4rem 0.9rem;border-radius:7px;cursor:pointer;font-size:0.82rem;font-weight:600;width:auto;"> Hinzufügen</button>
<button onclick="openCardModal('${d.lockId}', 'remove')" style="background:rgba(231,76,60,0.12);border:1px solid rgba(231,76,60,0.35);color:#e74c3c;padding:0.4rem 0.9rem;border-radius:7px;cursor:pointer;font-size:0.82rem;font-weight:600;width:auto;"> Entfernen</button>
</div>`;
if (d.openPicks > 0) {
html += `<div class="detail-row" style="margin-top:0.35rem;">
<span class="detail-label">Offene Züge</span>
<span class="detail-value warn">${d.openPicks}</span>
</div>`;
}
if (d.nextCardIn) {
const secUntil = Math.max(0, Math.round((new Date(d.nextCardIn) - Date.now()) / 1000));
html += `<div class="detail-row">
<span class="detail-label">Nächste Karte in</span>
<span class="detail-value">${fmtDuration(secUntil)}</span>
</div>`;
}
html += `</div>`;
// Einfrieren-Sektion
{
const isFrozen = d.frozenUntill && new Date(d.frozenUntill) > new Date();
html += `<div class="detail-section"><div class="detail-section-title">Einfrieren</div>`;
if (isFrozen) {
const frozenUntil = new Date(d.frozenUntill);
const frozenLabel = d.isFrozenByKeyholder ? '❄️ Eingefroren bis' : '❄️ Eingefroren bis (Aufgabe)';
html += `<div class="detail-row">
<span class="detail-label">${frozenLabel}</span>
<span class="detail-value danger">${frozenUntil.toLocaleString('de-DE')}</span>
</div>`;
if (d.isFrozenByKeyholder) {
html += `<div style="margin-top:0.5rem;">
<button onclick="openUnfreezeModal('${d.lockId}')" style="background:rgba(52,152,219,0.15);border:1px solid rgba(52,152,219,0.4);color:#3498db;padding:0.4rem 0.9rem;border-radius:7px;cursor:pointer;font-size:0.82rem;font-weight:600;width:auto;">❄️ Entfrieren</button>
</div>`;
}
} else if (!d.currentTask) {
html += `<div style="margin-top:0.25rem;">
<button onclick="openFreezeModal('${d.lockId}')" style="background:rgba(52,152,219,0.15);border:1px solid rgba(52,152,219,0.4);color:#3498db;padding:0.4rem 0.9rem;border-radius:7px;cursor:pointer;font-size:0.82rem;font-weight:600;width:auto;">❄️ Einfrieren</button>
</div>`;
} else {
html += `<div class="detail-row"><span class="detail-label">Status</span><span class="detail-value" style="color:var(--color-muted);">Nicht eingefroren</span></div>`;
}
html += `</div>`;
}
// Aktuelle Aufgabe
if (d.currentTask) {
let remainingHtml = '';
if (d.taskFrozenUntil && new Date(d.taskFrozenUntil) > new Date()) {
const secUntil = Math.max(0, Math.round((new Date(d.taskFrozenUntil) - Date.now()) / 1000));
remainingHtml = `<div style="font-size:0.78rem;color:#2ecc71;margin-top:0.35rem;">⏱ Noch: ${fmtDuration(secUntil)}</div>`;
}
const taskDescHtml = d.currentTaskDescription
? `<div style="font-size:0.82rem;color:var(--color-muted);margin-top:0.25rem;line-height:1.4;">${esc(d.currentTaskDescription)}</div>` : '';
html += `<div class="detail-section">
<div class="detail-section-title">Aktuelle Aufgabe</div>
<div style="border:1px solid rgba(46,204,113,0.5);border-radius:8px;padding:0.6rem 0.75rem;background:rgba(46,204,113,0.06);">
<div style="font-size:0.9rem;font-weight:600;line-height:1.5;">${esc(d.currentTask)}</div>
${taskDescHtml}
${remainingHtml}
</div>
</div>`;
}
// Aufgaben-Sektion
if (d.hasTasks) {
html += `<div class="detail-section"><div class="detail-section-title">Aufgaben</div>`;
// Ausstehende gestellte Aufgaben
const pending = d.pendingAssignedTasks || [];
if (pending.length > 0) {
pending.forEach(t => {
const gestellt = new Date(t.assignedAt).toLocaleString('de-DE', {day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'});
const fällig = new Date(t.acceptDeadline).toLocaleString('de-DE', {day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'});
const penParts = [];
if (t.penaltyFreezeMinutes > 0) penParts.push(`❄️ ${t.penaltyFreezeMinutes} Min. Freeze`);
if (t.penaltyRedCards > 0) penParts.push(`🔴 ${t.penaltyRedCards} rote Karte${t.penaltyRedCards > 1 ? 'n' : ''}`);
const penStr = penParts.length > 0 ? penParts.join(', ') : 'Keine Strafe';
const tTitle = t.taskTitle || t.taskText || '';
const tDescHtml = t.taskDescription
? `<div style="font-size:0.78rem;color:var(--color-muted);margin-bottom:0.2rem;">${esc(t.taskDescription)}</div>` : '';
const tMinsHtml = t.taskMinutes > 0
? `<div style="font-size:0.78rem;color:var(--color-muted);">Zeit: ${t.taskMinutes} Min.</div>` : '';
html += `<div style="background:rgba(155,89,182,0.08);border:1px solid rgba(155,89,182,0.25);border-radius:8px;padding:0.55rem 0.7rem;margin-bottom:0.4rem;display:flex;align-items:flex-start;gap:0.5rem;">
<div style="flex:1;min-width:0;">
<div style="font-size:0.9rem;font-weight:600;margin-bottom:0.2rem;">${esc(tTitle)}</div>
${tDescHtml}
${tMinsHtml}
<div style="font-size:0.78rem;color:var(--color-muted);">Gestellt: ${gestellt}</div>
<div style="font-size:0.78rem;color:var(--color-muted);">Fällig bis: ${fällig}</div>
<div style="font-size:0.78rem;color:#e67e22;margin-top:0.2rem;">Strafe: ${penStr}</div>
</div>
<button onclick="deleteAssignedTask('${d.lockId}', '${t.taskId}')" title="Aufgabe zurückziehen"
style="background:none;border:none;color:var(--color-muted);font-size:1.1rem;cursor:pointer;padding:0.1rem 0.3rem;line-height:1;flex-shrink:0;"
onmouseover="this.style.color='#e74c3c'" onmouseout="this.style.color='var(--color-muted)'">✕</button>
</div>`;
});
} else {
html += `<div style="font-size:0.85rem;color:var(--color-muted);padding:0.35rem 0;">Aktuell keine gestellten Aufgaben.</div>`;
}
// Aufgabe stellen Button (max. 5 offene Aufgaben)
const pendingCount = (d.pendingAssignedTasks || []).length;
if (pendingCount < 5) {
html += `<div style="margin-top:0.6rem;display:flex;align-items:center;gap:0.75rem;">
<button onclick="openAssignTaskModal('${d.lockId}')" style="background:rgba(155,89,182,0.15);border:1px solid rgba(155,89,182,0.4);color:#9b59b6;padding:0.4rem 0.9rem;border-radius:7px;cursor:pointer;font-size:0.82rem;font-weight:600;width:auto;">✅ Aufgabe stellen</button>
<span style="font-size:0.78rem;color:var(--color-muted);">${pendingCount}/5 gestellt</span>
</div>`;
} else {
html += `<div style="margin-top:0.6rem;font-size:0.82rem;color:var(--color-muted);">Maximale Anzahl offener Aufgaben (5) erreicht.</div>`;
}
html += `</div>`;
}
// Ausstehende Task-Karten-Entscheidungen (Keyholder-Modus)
if (d.taskCardMode === 'KEYHOLDER' && d.pendingTaskChoices && d.pendingTaskChoices.length > 0) {
html += `<div class="detail-section">
<div class="detail-section-title">🃏 Aufgaben-Auswahl ausstehend</div>
<div style="font-size:0.82rem;color:var(--color-muted);margin-bottom:0.6rem;">
Die Lockee hat eine Aufgaben-Karte gezogen. Wähle eine Aufgabe aus:
</div>`;
d.pendingTaskChoices.forEach(choice => {
const since = new Date(choice.createdAt).toLocaleString('de-DE', {day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'});
html += `<div style="background:rgba(52,152,219,0.08);border:1px solid rgba(52,152,219,0.3);border-radius:8px;padding:0.6rem 0.7rem;margin-bottom:0.6rem;">
<div style="font-size:0.78rem;color:var(--color-muted);margin-bottom:0.5rem;">Gezogen am: ${since}</div>
<div style="display:flex;flex-direction:column;gap:0.3rem;">`;
(choice.tasks || []).forEach(t => {
const desc = t.description ? `<div style="font-size:0.75rem;color:var(--color-muted);margin-top:0.1rem;">${esc(t.description)}</div>` : '';
const mins = t.minutes > 0 ? `<span style="font-size:0.75rem;color:var(--color-muted);margin-left:0.4rem;">⏱ ${t.minutes} Min.</span>` : '';
const descEscJs = (t.description || '').replace(/'/g, "\\'").replace(/\n/g, ' ');
const titleEscJs = t.title.replace(/'/g, "\\'");
html += `<button onclick="openChooseTaskPopup('${choice.choiceId}', ${t.index}, '${d.lockId}', '${titleEscJs}', '${descEscJs}', ${t.minutes || 0})"
style="background:rgba(52,152,219,0.12);border:1px solid rgba(52,152,219,0.35);border-radius:6px;padding:0.4rem 0.6rem;cursor:pointer;text-align:left;color:var(--color-text);"
onmouseover="this.style.background='rgba(52,152,219,0.25)'" onmouseout="this.style.background='rgba(52,152,219,0.12)'">
<div style="font-size:0.85rem;font-weight:600;">${esc(t.title)}${mins}</div>
${desc}
</button>`;
});
html += `</div></div>`;
});
html += `</div>`;
}
// Hygiene
if (d.hygieneEnabled) {
html += `<div class="detail-section">
<div class="detail-section-title">Hygiene-Öffnung</div>`;
if (d.hygieneOpeningActive) {
html += `<div class="detail-row"><span class="detail-label">Status</span><span class="detail-value warn">Öffnung aktiv</span></div>`;
} else if (d.hygieneOpeningDue) {
html += `<div class="detail-row"><span class="detail-label">Status</span><span class="detail-value ok">Verfügbar</span></div>`;
} else {
html += `<div class="detail-row">
<span class="detail-label">Verfügbar in</span>
<span class="detail-value">${fmtDuration(d.hygieneSecondsRemaining)}</span>
</div>`;
}
html += `</div>`;
}
// Verifikation
if (d.requiresVerification) {
html += `<div class="detail-section">
<div class="detail-section-title">Verifikation heute</div>`;
if (!d.verificationDoneToday) {
html += `<div class="detail-row"><span class="detail-label">Status</span><span class="detail-value danger">Warte auf Verifikation</span></div>`;
} else if (!d.verificationMyVote) {
html += `<div class="detail-row">
<span class="detail-label">Status</span>
<span class="detail-value warn">Verifikation ausstehend &nbsp;<a href="#" class="prufen-link" style="font-size:0.82rem;color:var(--color-primary);font-weight:600;">Prüfen →</a></span>
</div>`;
} else if (d.verificationMyVote === 'upvote') {
html += `<div class="detail-row"><span class="detail-label">Status</span><span class="detail-value ok">✓ Erledigt</span></div>`;
} else {
html += `<div class="detail-row"><span class="detail-label">Status</span><span class="detail-value danger">✗ Abgelehnt</span></div>`;
}
html += `</div>`;
}
// Verletzungen
if (d.hygieneViolations && d.hygieneViolations.length > 0) {
html += `<div class="detail-section">
<div class="detail-section-title">Verletzungen (letzte ${d.hygieneViolations.length})</div>`;
d.hygieneViolations.forEach(v => {
const dt = new Date(v.time).toLocaleString('de-DE', { day:'2-digit', month:'2-digit', year:'numeric', hour:'2-digit', minute:'2-digit' });
let label;
if (v.openingReason === 'TTLOCK_UNAUTHORIZED') {
label = '<span style="color:var(--color-primary);font-weight:600;">Unerlaubte Öffnung</span>';
} else {
label = `<span style="color:var(--color-primary);font-weight:600;">+${v.overtimeMinutes} Min. Hygiene-Überschreitung</span>`;
}
html += `<div class="violation-item">${dt} · ${label}</div>`;
});
html += `</div>`;
}
// Gestartet am
if (d.startTime) {
html += `<div style="font-size:0.78rem;color:var(--color-muted);margin-top:0.5rem;">
Gestartet am ${new Date(d.startTime).toLocaleDateString('de-DE')}
</div>`;
}
// Notfall-Entsperrung Banner
if (d.emergencyUnlockRequested && !d.keyholderRequestedUnlock) {
html += `<div style="display:flex;align-items:flex-start;gap:0.75rem;background:rgba(231,76,60,0.12);border:2px solid rgba(231,76,60,0.5);border-radius:10px;padding:1rem 1.1rem;margin-bottom:0.5rem;">
<span style="font-size:1.4rem;flex-shrink:0;">🆘</span>
<div>
<div style="font-weight:700;font-size:0.95rem;color:#e74c3c;margin-bottom:0.25rem;">Notfall-Entsperrung angefordert!</div>
<div style="font-size:0.85rem;color:var(--color-muted);line-height:1.5;">Deine Lockee bittet dringend um Freigabe des Locks. Reagiere innerhalb einer Stunde oder das Lock öffnet sich automatisch.</div>
<div style="margin-top:0.6rem;">
<button onclick="requestUnlock('${d.lockId}')"
style="background:rgba(46,204,113,0.2);border:1px solid rgba(46,204,113,0.5);color:#2ecc71;padding:0.4rem 0.9rem;border-radius:7px;cursor:pointer;font-size:0.85rem;font-weight:600;width:auto;">
🔓 Jetzt freigeben
</button>
</div>
</div>
</div>`;
}
// Lock entsperren (ganz unten)
{
const alreadyRequested = !!d.keyholderRequestedUnlock;
html += `<div class="detail-section"><div class="detail-section-title">Lock entsperren</div>`;
if (alreadyRequested) {
html += `<div style="font-size:0.85rem;color:var(--color-muted);">✅ Unlock wurde angefordert die Lockee erhält beim nächsten Laden ihren Entsperrcode.</div>`;
} else {
html += `
<div id="unlockConfirm_${d.lockId}">
<button onclick="showUnlockConfirm('${d.lockId}')"
style="background:rgba(46,204,113,0.15);border:1px solid rgba(46,204,113,0.4);color:#2ecc71;padding:0.4rem 0.9rem;border-radius:7px;cursor:pointer;font-size:0.82rem;font-weight:600;width:auto;">
🔓 Lock freigeben
</button>
</div>`;
}
html += `</div>`;
}
return html;
}
// ── Hilfsfunktionen ──
function fmtDuration(seconds) {
if (seconds <= 0) return 'Jetzt';
const d = Math.floor(seconds / 86400);
const h = Math.floor((seconds % 86400) / 3600);
const m = Math.floor((seconds % 3600) / 60);
if (d > 0) return `${d}d ${h}h`;
if (h > 0) return `${h}h ${m}m`;
if (m > 0) return `${m}m`;
return '< 1m';
}
// ── Verifikation prüfen ──
let verificationVoteLockId = null;
function openVerificationModal(lockId) {
const d = lockDetailCache[lockId];
if (!d || !d.verificationImage) return;
verificationVoteLockId = lockId;
document.getElementById('verificationVoteImg').src = 'data:image/jpeg;base64,' + d.verificationImage;
document.getElementById('verificationVoteBtnUp').disabled = false;
document.getElementById('verificationVoteBtnDown').disabled = false;
document.getElementById('verificationVoteModal').style.display = 'flex';
}
function closeVerificationModal() {
document.getElementById('verificationVoteModal').style.display = 'none';
verificationVoteLockId = null;
}
async function submitVerificationVote(upvote) {
const lockId = verificationVoteLockId;
const d = lockDetailCache[lockId];
if (!d?.verificationTodayId) return;
const btnUp = document.getElementById('verificationVoteBtnUp');
const btnDown = document.getElementById('verificationVoteBtnDown');
btnUp.disabled = btnDown.disabled = true;
try {
const res = await fetch('/verification/' + d.verificationTodayId + '/vote/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ upvote })
});
if (res.ok || res.status === 202) {
closeVerificationModal();
await reloadLockDetail(lockId);
} else {
btnUp.disabled = btnDown.disabled = false;
}
} catch(e) { btnUp.disabled = btnDown.disabled = false; }
}
document.getElementById('verificationVoteModal').addEventListener('click', e => {
if (e.target === e.currentTarget) closeVerificationModal();
});
// ── Karten hinzufügen / entfernen ──
let cardEditLockId = null;
let cardEditMode = null;
function openCardModal(lockId, mode) {
cardEditLockId = lockId;
cardEditMode = mode;
const d = lockDetailCache[lockId] || {};
const counts = d.cardCounts || {};
document.getElementById('cardEditTitle').textContent =
mode === 'add' ? 'Karten hinzufügen' : 'Karten entfernen';
document.getElementById('cardEditError').style.display = 'none';
document.getElementById('cardEditNotifyDetailed').checked = true;
document.getElementById('cardEditSubmit').disabled = false;
const inputs = document.getElementById('cardEditInputs');
inputs.innerHTML = '';
Object.entries(CARD_LABELS).forEach(([type, info]) => {
const current = counts[type] || 0;
if (mode === 'remove' && current === 0) return;
// Aufgabenkarten beim Hinzufügen nur anzeigen, wenn Aufgaben definiert sind
if (mode === 'add' && type === 'TASK' && !d.hasTasks) return;
// Grünkarten-Plausi: letzte grüne Karte darf nicht entfernt werden
const maxRemove = type === 'GREEN' ? Math.max(0, current - 1) : current;
const max = mode === 'add' ? 20 : maxRemove;
const row = document.createElement('div');
row.style.cssText = 'display:flex;align-items:center;gap:0.75rem;padding:0.3rem 0;';
row.innerHTML = `
<img src="${info.img}" alt="${info.name}" style="width:28px;height:auto;border-radius:4px;flex-shrink:0;">
<span style="flex:1;font-size:0.88rem;">${info.name}</span>
${mode === 'remove'
? `<span style="font-size:0.78rem;color:var(--color-muted);">${current} im Stapel${type === 'GREEN' && current === 1 ? ' · nicht entfernbar' : ''}</span>`
: ''}
<input type="number" min="0" max="${max}" value="0" data-type="${type}" ${max === 0 ? 'disabled' : ''}
style="width:58px;padding:0.3rem 0.4rem;border-radius:6px;border:1px solid var(--color-secondary);background:var(--color-secondary);color:var(--color-text);font-size:0.9rem;text-align:center;">`;
inputs.appendChild(row);
});
document.getElementById('cardEditModal').style.display = 'flex';
}
function closeCardModal() {
document.getElementById('cardEditModal').style.display = 'none';
cardEditLockId = null;
cardEditMode = null;
}
async function submitCardEdit() {
const btn = document.getElementById('cardEditSubmit');
btn.disabled = true;
document.getElementById('cardEditError').style.display = 'none';
const cards = {};
document.querySelectorAll('#cardEditInputs input[type=number]').forEach(inp => {
const v = parseInt(inp.value) || 0;
if (v > 0) cards[inp.dataset.type] = v;
});
if (Object.keys(cards).length === 0) { btn.disabled = false; closeCardModal(); return; }
const notifyDetailed = document.getElementById('cardEditNotifyDetailed').checked;
const lockId = cardEditLockId;
const mode = cardEditMode;
try {
const res = await fetch(`/keyholder/as-keyholder/${lockId}/cards/${mode}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ cards, notifyDetailed })
});
if (res.ok || res.status === 204) {
closeCardModal();
await reloadLockDetail(lockId);
} else {
const data = await res.json().catch(() => ({}));
const errEl = document.getElementById('cardEditError');
errEl.textContent = data.error || 'Fehler beim Speichern.';
errEl.style.display = '';
btn.disabled = false;
}
} catch(e) { btn.disabled = false; }
}
// Modals bei Klick außerhalb / Esc schließen
document.getElementById('cardEditModal').addEventListener('click', e => {
if (e.target === e.currentTarget) closeCardModal();
});
document.getElementById('assignTaskModal').addEventListener('click', e => {
if (e.target === e.currentTarget) closeAssignTaskModal();
});
document.getElementById('freezeModal').addEventListener('click', e => {
if (e.target === e.currentTarget) closeFreezeModal();
});
document.getElementById('unfreezeModal').addEventListener('click', e => {
if (e.target === e.currentTarget) closeUnfreezeModal();
});
document.addEventListener('keydown', e => {
if (e.key !== 'Escape') return;
if (document.getElementById('assignTaskModal').style.display === 'flex') closeAssignTaskModal();
if (document.getElementById('freezeModal').style.display === 'flex') closeFreezeModal();
if (document.getElementById('unfreezeModal').style.display === 'flex') closeUnfreezeModal();
});
// ── Aufgabe stellen ──
let assignTaskLockId = null;
let assignTaskSelectedIdx = null;
function atTpChange(delta, seg) {
let d = parseInt(document.getElementById('at_d').value) || 0;
let h = parseInt(document.getElementById('at_h').value) || 0;
let m = parseInt(document.getElementById('at_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 = 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 = h%24; }
if (h < 0) { const b = Math.ceil(-h/24); d -= b; h += b*24; }
if (d < 0) d = 0;
document.getElementById('at_d').value = d;
document.getElementById('at_h').value = String(h).padStart(2,'0');
document.getElementById('at_m').value = String(m).padStart(2,'0');
}
function atFreezeTpChange(delta, seg) {
let d = parseInt(document.getElementById('atf_d').value) || 0;
let h = parseInt(document.getElementById('atf_h').value) || 0;
let m = parseInt(document.getElementById('atf_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 = 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 = h%24; }
if (h < 0) { const b = Math.ceil(-h/24); d -= b; h += b*24; }
if (d < 0) d = 0;
document.getElementById('atf_d').value = d;
document.getElementById('atf_h').value = String(h).padStart(2,'0');
document.getElementById('atf_m').value = String(m).padStart(2,'0');
}
function atTpToMinutes(prefix) {
return (parseInt(document.getElementById(prefix+'_d').value)||0) * 24*60
+ (parseInt(document.getElementById(prefix+'_h').value)||0) * 60
+ (parseInt(document.getElementById(prefix+'_m').value)||0);
}
function atRedChange(delta) {
const inp = document.getElementById('atRedCount');
let v = parseInt(inp.value) || 1;
v = Math.max(1, Math.min(20, v + delta));
inp.value = v;
}
function toggleAtFreezeFields() {
const on = document.getElementById('atPenaltyFreezeEnabled').checked;
const el = document.getElementById('atPenaltyFreezeFields');
el.style.opacity = on ? '1' : '0.4';
el.style.pointerEvents = on ? '' : 'none';
}
function toggleAtRedFields() {
const on = document.getElementById('atPenaltyRedEnabled').checked;
const el = document.getElementById('atPenaltyRedFields');
el.style.opacity = on ? '1' : '0.4';
el.style.pointerEvents = on ? '' : 'none';
}
let assignTaskComboActive = -1;
function openAssignTaskModal(lockId) {
const d = lockDetailCache[lockId];
if (!d || !d.taskList || d.taskList.length === 0) return;
assignTaskLockId = lockId;
assignTaskSelectedIdx = null;
// Reset
document.getElementById('at_d').value = '0';
document.getElementById('at_h').value = '01';
document.getElementById('at_m').value = '00';
document.getElementById('atf_d').value = '0';
document.getElementById('atf_h').value = '04';
document.getElementById('atf_m').value = '00';
document.getElementById('atRedCount').value = '1';
document.getElementById('atPenaltyFreezeEnabled').checked = false;
document.getElementById('atPenaltyRedEnabled').checked = false;
toggleAtFreezeFields();
toggleAtRedFields();
document.getElementById('assignTaskError').style.display = 'none';
// Combo zurücksetzen
const input = document.getElementById('assignTaskInput');
const dropdown = document.getElementById('assignTaskDropdown');
const hidden = document.getElementById('assignTaskHiddenIdx');
input.value = '';
hidden.value = '';
dropdown.style.display = 'none';
assignTaskComboActive = -1;
// Alte Listener entfernen (Clone-Trick)
const newInput = input.cloneNode(true);
input.parentNode.replaceChild(newInput, input);
function renderTaskDropdown(query) {
const q = query.toLowerCase().trim();
const filtered = d.taskList
.map((t, i) => ({ t, i }))
.filter(({ t }) => (t.title || t.text || '').toLowerCase().includes(q));
dropdown.innerHTML = '';
assignTaskComboActive = -1;
if (filtered.length === 0) {
dropdown.innerHTML = `<div style="padding:0.55rem 0.85rem;font-size:0.85rem;color:var(--color-muted);font-style:italic;">Keine Treffer.</div>`;
} else {
filtered.forEach(({ t, i }) => {
const label = t.title || t.text || '';
const div = document.createElement('div');
div.style.cssText = 'padding:0.5rem 0.85rem;cursor:pointer;font-size:0.88rem;color:var(--color-text);';
div.innerHTML = `<div style="font-weight:600;">${esc(label)}</div>` +
(t.description ? `<div style="font-size:0.75rem;color:var(--color-muted);">${esc(t.description)}</div>` : '') +
(t.minutes ? `<div style="font-size:0.75rem;color:var(--color-muted);">Dauer: ${fmtDuration(t.minutes * 60)}</div>` : '');
div.addEventListener('mouseover', () => {
dropdown.querySelectorAll('[data-combo]').forEach(el => el.style.background = '');
div.style.background = 'var(--color-secondary)';
});
div.addEventListener('mouseout', () => div.style.background = '');
div.dataset.combo = i;
div.addEventListener('mousedown', e => {
e.preventDefault();
selectAssignTask(i, label);
});
dropdown.appendChild(div);
});
}
dropdown.style.display = '';
}
function selectAssignTask(idx, text) {
document.getElementById('assignTaskHiddenIdx').value = idx;
document.getElementById('assignTaskInput').value = text;
assignTaskSelectedIdx = idx;
dropdown.style.display = 'none';
}
newInput.addEventListener('input', () => {
document.getElementById('assignTaskHiddenIdx').value = '';
assignTaskSelectedIdx = null;
renderTaskDropdown(newInput.value);
});
newInput.addEventListener('focus', () => renderTaskDropdown(newInput.value));
newInput.addEventListener('blur', () => setTimeout(() => { dropdown.style.display = 'none'; }, 150));
newInput.addEventListener('keydown', e => {
const opts = dropdown.querySelectorAll('[data-combo]');
if (dropdown.style.display === 'none' || opts.length === 0) return;
if (e.key === 'ArrowDown') {
e.preventDefault();
assignTaskComboActive = Math.min(assignTaskComboActive + 1, opts.length - 1);
opts.forEach((o, i) => o.style.background = i === assignTaskComboActive ? 'var(--color-secondary)' : '');
} else if (e.key === 'ArrowUp') {
e.preventDefault();
assignTaskComboActive = Math.max(assignTaskComboActive - 1, 0);
opts.forEach((o, i) => o.style.background = i === assignTaskComboActive ? 'var(--color-secondary)' : '');
} else if (e.key === 'Enter' && assignTaskComboActive >= 0) {
e.preventDefault();
const chosen = opts[assignTaskComboActive];
selectAssignTask(parseInt(chosen.dataset.combo), chosen.querySelector('div').textContent);
} else if (e.key === 'Escape') {
dropdown.style.display = 'none';
}
});
document.getElementById('assignTaskModal').style.display = 'flex';
newInput.focus();
}
function closeAssignTaskModal() {
document.getElementById('assignTaskModal').style.display = 'none';
document.getElementById('assignTaskDropdown').style.display = 'none';
assignTaskLockId = null;
assignTaskSelectedIdx = null;
}
async function submitAssignTask() {
const errEl = document.getElementById('assignTaskError');
errEl.style.display = 'none';
if (assignTaskSelectedIdx === null) {
errEl.textContent = 'Bitte eine Aufgabe auswählen.';
errEl.style.display = '';
return;
}
const deadlineMinutes = atTpToMinutes('at');
if (deadlineMinutes < 1) {
errEl.textContent = 'Bitte eine Annahme-Frist angeben.';
errEl.style.display = '';
return;
}
const freezeEnabled = document.getElementById('atPenaltyFreezeEnabled').checked;
const redEnabled = document.getElementById('atPenaltyRedEnabled').checked;
if (!freezeEnabled && !redEnabled) {
errEl.textContent = 'Bitte mindestens eine Strafe festlegen.';
errEl.style.display = '';
return;
}
const penaltyFreezeMinutes = freezeEnabled ? atTpToMinutes('atf') : null;
const penaltyRedCards = redEnabled ? parseInt(document.getElementById('atRedCount').value) || 1 : null;
try {
const res = await fetch(`/keyholder/as-keyholder/${assignTaskLockId}/assign-task`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
taskIndex: assignTaskSelectedIdx,
acceptDeadlineMinutes: deadlineMinutes,
penaltyFreezeMinutes,
penaltyRedCards
})
});
if (res.ok || res.status === 204) {
const lockId = assignTaskLockId;
closeAssignTaskModal();
await reloadLockDetail(lockId);
} else {
const data = await res.json().catch(() => ({}));
errEl.textContent = data.error || 'Fehler beim Stellen der Aufgabe.';
errEl.style.display = '';
}
} catch(e) {
errEl.textContent = 'Fehler beim Stellen der Aufgabe.';
errEl.style.display = '';
}
}
// ── Lock entsperren ──
function showUnlockConfirm(lockId) {
const el = document.getElementById('unlockConfirm_' + lockId);
if (!el) return;
el.innerHTML = `
<div style="background:rgba(46,204,113,0.08);border:1px solid rgba(46,204,113,0.3);border-radius:8px;padding:0.75rem 1rem;">
<div style="font-size:0.88rem;font-weight:600;margin-bottom:0.6rem;color:var(--color-text);">Soll das Lock wirklich geöffnet werden?</div>
<div style="display:flex;gap:0.5rem;">
<button onclick="requestUnlock('${lockId}')"
style="background:rgba(46,204,113,0.2);border:1px solid rgba(46,204,113,0.5);color:#2ecc71;padding:0.35rem 0.9rem;border-radius:6px;cursor:pointer;font-size:0.82rem;font-weight:600;width:auto;">
✓ Ja, freigeben
</button>
<button onclick="hideUnlockConfirm('${lockId}')"
style="background:transparent;border:1px solid var(--color-secondary);color:var(--color-muted);padding:0.35rem 0.9rem;border-radius:6px;cursor:pointer;font-size:0.82rem;width:auto;">
Abbrechen
</button>
</div>
</div>`;
}
function hideUnlockConfirm(lockId) {
const el = document.getElementById('unlockConfirm_' + lockId);
if (!el) return;
el.innerHTML = `
<button onclick="showUnlockConfirm('${lockId}')"
style="background:rgba(46,204,113,0.15);border:1px solid rgba(46,204,113,0.4);color:#2ecc71;padding:0.4rem 0.9rem;border-radius:7px;cursor:pointer;font-size:0.82rem;font-weight:600;width:auto;">
🔓 Lock freigeben
</button>`;
}
async function requestUnlock(lockId) {
try {
const endpoint = (lockDetailCache[lockId]?.lockType || lockTypeMap[lockId]) === 'TIMELOCK'
? `/keyholder/timelock/as-keyholder/${lockId}/request-unlock`
: `/keyholder/as-keyholder/${lockId}/request-unlock`;
const res = await fetch(endpoint, { method: 'POST' });
if (res.ok || res.status === 204) {
await reloadLockDetail(lockId);
}
} catch(e) { console.error(e); }
}
// ── Einfrieren / Entfrieren ──
let freezeTargetLockId = null;
let unfreezeTargetLockId = null;
// Zeitpicker-Logik (Dauer)
function freezeTpChange(delta, seg) {
let d = parseInt(document.getElementById('freeze_d').value) || 0;
let h = parseInt(document.getElementById('freeze_h').value) || 0;
let m = parseInt(document.getElementById('freeze_m').value) || 0;
if (seg === 'm') m += delta;
else if (seg === 'h') h += delta;
else if (seg === 'd') d += delta;
if (m >= 60) { h += Math.floor(m / 60); m = 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 = h % 24; }
if (h < 0) { const b = Math.ceil(-h / 24); d -= b; h += b * 24; }
if (d < 0) d = 0;
document.getElementById('freeze_d').value = d;
document.getElementById('freeze_h').value = String(h).padStart(2, '0');
document.getElementById('freeze_m').value = String(m).padStart(2, '0');
}
function freezeTpToMinutes() {
const d = parseInt(document.getElementById('freeze_d').value) || 0;
const h = parseInt(document.getElementById('freeze_h').value) || 0;
const m = parseInt(document.getElementById('freeze_m').value) || 0;
return d * 24 * 60 + h * 60 + m;
}
function openFreezeModal(lockId) {
freezeTargetLockId = lockId;
// Default: 4h
document.getElementById('freeze_d').value = '0';
document.getElementById('freeze_h').value = '04';
document.getElementById('freeze_m').value = '00';
document.getElementById('freezeModalError').style.display = 'none';
document.getElementById('freezeModal').style.display = 'flex';
}
function closeFreezeModal() {
document.getElementById('freezeModal').style.display = 'none';
freezeTargetLockId = null;
}
async function submitFreeze() {
const lockId = freezeTargetLockId;
const minutes = freezeTpToMinutes();
const errEl = document.getElementById('freezeModalError');
if (minutes < 1) {
errEl.textContent = 'Bitte eine Dauer von mindestens 1 Minute angeben.';
errEl.style.display = '';
return;
}
errEl.style.display = 'none';
const frozenUntil = new Date(Date.now() + minutes * 60000).toISOString().slice(0, 19);
const freezeEndpoint = lockTypeMap[lockId] === 'TIMELOCK'
? `/keyholder/timelock/as-keyholder/${lockId}/freeze`
: `/keyholder/as-keyholder/${lockId}/freeze`;
try {
const res = await fetch(freezeEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ frozenUntil })
});
if (res.ok || res.status === 204) {
closeFreezeModal();
await reloadLockDetail(lockId);
} else {
const data = await res.json().catch(() => ({}));
errEl.textContent = data.error || 'Fehler beim Einfrieren.';
errEl.style.display = '';
}
} catch(e) {
errEl.textContent = 'Fehler beim Einfrieren.';
errEl.style.display = '';
}
}
function openUnfreezeModal(lockId) {
unfreezeTargetLockId = lockId;
document.getElementById('unfreezeModal').style.display = 'flex';
}
function closeUnfreezeModal() {
document.getElementById('unfreezeModal').style.display = 'none';
unfreezeTargetLockId = null;
}
async function submitUnfreeze() {
const lockId = unfreezeTargetLockId;
const unfreezeEndpoint = lockTypeMap[lockId] === 'TIMELOCK'
? `/keyholder/timelock/as-keyholder/${lockId}/freeze`
: `/keyholder/as-keyholder/${lockId}/freeze`;
try {
const res = await fetch(unfreezeEndpoint, { method: 'DELETE' });
if (res.ok || res.status === 204) {
closeUnfreezeModal();
await reloadLockDetail(lockId);
}
} catch(e) { console.error(e); }
}
async function chooseTaskForLock(choiceId, taskIndex, lockId) {
try {
const res = await fetch(`/task-card/keyholder/choices/${choiceId}/choose/${taskIndex}`, { method: 'POST' });
if (res.ok || res.status === 204) {
await reloadLockDetail(lockId);
}
} catch(e) { console.error(e); }
}
async function deleteAssignedTask(lockId, taskId) {
try {
const res = await fetch(`/keyholder/as-keyholder/${lockId}/assigned-tasks/${taskId}`, { method: 'DELETE' });
if (res.ok || res.status === 204) {
await reloadLockDetail(lockId);
}
} catch(e) { console.error(e); }
}
// ── Aufgaben-Auswahl-Popup ─────────────────────────────────────────────────
let _pendingChoiceId = null;
let _pendingTaskIndex = null;
let _pendingLockId = null;
function ctpFreezeTpChange(delta, seg) {
let d = parseInt(document.getElementById('ctpf_d').value) || 0;
let h = parseInt(document.getElementById('ctpf_h').value) || 0;
let m = parseInt(document.getElementById('ctpf_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 = 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 = h%24; }
if (h < 0) { const b = Math.ceil(-h/24); d -= b; h += b*24; }
if (d < 0) d = 0;
document.getElementById('ctpf_d').value = d;
document.getElementById('ctpf_h').value = String(h).padStart(2,'0');
document.getElementById('ctpf_m').value = String(m).padStart(2,'0');
}
function ctpFreezeToMinutes() {
return (parseInt(document.getElementById('ctpf_d').value)||0) * 24*60
+ (parseInt(document.getElementById('ctpf_h').value)||0) * 60
+ (parseInt(document.getElementById('ctpf_m').value)||0);
}
function ctpRedChange(delta) {
const inp = document.getElementById('ctpRedCount');
let v = parseInt(inp.value) || 1;
v = Math.max(1, Math.min(20, v + delta));
inp.value = v;
}
function toggleCtpFreezeFields() {
const on = document.getElementById('ctpPenaltyFreezeEnabled').checked;
const el = document.getElementById('ctpPenaltyFreezeFields');
el.style.opacity = on ? '1' : '0.4';
el.style.pointerEvents = on ? '' : 'none';
}
function toggleCtpRedFields() {
const on = document.getElementById('ctpPenaltyRedEnabled').checked;
const el = document.getElementById('ctpPenaltyRedFields');
el.style.opacity = on ? '1' : '0.4';
el.style.pointerEvents = on ? '' : 'none';
}
function openChooseTaskPopup(choiceId, taskIndex, lockId, taskTitle, taskDesc, taskMinutes) {
_pendingChoiceId = choiceId;
_pendingTaskIndex = taskIndex;
_pendingLockId = lockId;
document.getElementById('ctpTaskTitle').textContent = taskTitle;
document.getElementById('ctpTaskDesc').textContent = taskDesc || '';
document.getElementById('ctpTaskDesc').style.display = taskDesc ? '' : 'none';
document.getElementById('ctpTaskMins').textContent = taskMinutes > 0 ? `${taskMinutes} Min.` : '';
// Felder zurücksetzen
document.getElementById('ctpPenaltyFreezeEnabled').checked = false;
document.getElementById('ctpPenaltyRedEnabled').checked = false;
document.getElementById('ctpf_d').value = '0';
document.getElementById('ctpf_h').value = '04';
document.getElementById('ctpf_m').value = '00';
document.getElementById('ctpRedCount').value = '1';
toggleCtpFreezeFields();
toggleCtpRedFields();
document.getElementById('ctpModal').style.display = 'flex';
}
function closeChooseTaskPopup() {
document.getElementById('ctpModal').style.display = 'none';
_pendingChoiceId = _pendingTaskIndex = _pendingLockId = null;
}
async function confirmChooseTask() {
if (!_pendingChoiceId) return;
const freezeEnabled = document.getElementById('ctpPenaltyFreezeEnabled').checked;
const redEnabled = document.getElementById('ctpPenaltyRedEnabled').checked;
if (!freezeEnabled && !redEnabled) {
document.getElementById('ctpPenaltyError').style.display = '';
return;
}
document.getElementById('ctpPenaltyError').style.display = 'none';
const freezeVal = freezeEnabled ? (ctpFreezeToMinutes() || null) : null;
const redVal = redEnabled ? (parseInt(document.getElementById('ctpRedCount').value) || null) : null;
try {
const res = await fetch(
`/task-card/keyholder/choices/${_pendingChoiceId}/choose/${_pendingTaskIndex}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ penaltyFreezeMinutes: freezeVal, penaltyRedCards: redVal })
}
);
if (res.ok || res.status === 204) {
closeChooseTaskPopup();
await loadLocks();
}
} catch(e) { console.error(e); }
}
// Initial laden, dann ggf. per URL-Parameter ein Lock vorauswählen / Tab wiederherstellen
loadLocks().then(() => {
restoreTabFromUrl();
const params = new URLSearchParams(window.location.search);
const preselect = params.get('lockId');
if (preselect) {
const card = document.querySelector(`[data-lock-id="${preselect}"]`);
if (card) {
card.scrollIntoView({ behavior: 'smooth', block: 'start' });
toggleLock(card, preselect);
}
}
});
// Automatische Aktualisierung alle 60 Sekunden für geöffnete Lock-Details
setInterval(() => {
document.querySelectorAll('#locksGrid .lock-card.open').forEach(card => {
const lockId = card.dataset.lockId;
if (lockId) reloadLockDetail(lockId);
});
}, 60000);
// ── Tab-Switching ──────────────────────────────────────────────────────────
function switchTab(tab) {
document.querySelectorAll('.kh-tab').forEach(b => b.classList.remove('active'));
event.currentTarget.classList.add('active');
document.getElementById('tabLockees').style.display = tab === 'lockees' ? '' : 'none';
document.getElementById('tabOffers').style.display = tab === 'offers' ? '' : 'none';
if (tab === 'offers') loadOffers();
const url = new URL(window.location.href);
if (tab === 'lockees') url.searchParams.delete('tab');
else url.searchParams.set('tab', tab);
history.replaceState(null, '', url.toString());
}
function restoreTabFromUrl() {
const tab = new URLSearchParams(window.location.search).get('tab');
if (tab === 'offers') {
const btn = document.querySelector('.kh-tab:nth-child(2)');
if (btn) { btn.click(); }
}
}
// ── Keyholder-Angebote ────────────────────────────────────────────────────
let _offerTemplates = [];
let _editOfferId = null;
function esc2(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }
const GENDER_LABELS = { WEIBLICH: 'Weiblich', MAENNLICH: 'Männlich', DIVERS: 'Divers' };
async function loadOffers() {
const res = await fetch('/keyholder-offers/mine');
if (!res.ok) return;
const offers = await res.json();
const list = document.getElementById('offersList');
const empty = document.getElementById('offersEmpty');
list.innerHTML = '';
if (offers.length === 0) { empty.style.display = ''; return; }
empty.style.display = 'none';
offers.forEach(o => list.appendChild(buildOfferCard(o)));
}
function buildOfferCard(o) {
const genderTags = (o.targetGenders && o.targetGenders.length > 0)
? o.targetGenders.map(g => `<span class="offer-badge">${esc2(GENDER_LABELS[g] || g)}</span>`).join('')
: '<span class="offer-badge">Alle</span>';
const modeBadge = o.directStart
? '<span class="offer-badge direct">Direktstart</span>'
: '<span class="offer-badge confirm">Mit Bestätigung</span>';
const typeIcon = o.templateType === 'TIMELOCK'
? `<div class="template-type-icon"><span class="icon-base">🕐</span><span class="icon-lock">🔒</span></div>`
: `<div class="template-type-icon"><img src="img/card.png" class="icon-base" alt="Karten-Lock"><span class="icon-lock">🔒</span></div>`;
const div = document.createElement('div');
div.className = 'offer-card';
div.style.cursor = 'pointer';
div.innerHTML = `
${typeIcon}
<div class="offer-card-body" style="flex:1;min-width:0;">
<div class="offer-card-name">${esc2(o.templateName)}</div>
<div class="offer-card-meta">
${modeBadge} ${genderTags}
<span class="offer-badge">✓ ${o.acceptanceCount}× angenommen</span>
</div>
</div>
<button class="btn-offer-del" onclick="event.stopPropagation();deleteOffer('${o.id}')">Entfernen</button>`;
div.addEventListener('click', () => openEditOfferModal(o));
return div;
}
async function deleteOffer(id) {
if (!confirm('Angebot wirklich entfernen?')) return;
const res = await fetch(`/keyholder-offers/${id}`, { method: 'DELETE' });
if (res.ok || res.status === 204) loadOffers();
else alert('Fehler beim Entfernen.');
}
// ── Angebot erstellen / bearbeiten ────────────────────────────────────────
async function _openOfferModal() {
document.getElementById('createOfferError').style.display = 'none';
document.getElementById('createOfferSubmitBtn').disabled = false;
// Templates laden (eigene + abonnierte)
const [ownRes, subRes] = await Promise.all([
fetch('/templates/mine'),
fetch('/templates/subscribed')
]);
const own = ownRes.ok ? await ownRes.json() : [];
const sub = subRes.ok ? await subRes.json() : [];
const ownIds = new Set(own.map(t => t.templateId));
_offerTemplates = [...own, ...sub.filter(t => !ownIds.has(t.templateId))];
setupOfferTemplateCombo();
}
async function openCreateOfferModal() {
_editOfferId = null;
document.getElementById('offerModalTitle').textContent = '🔑 Keyholder-Angebot erstellen';
document.getElementById('createOfferSubmitBtn').textContent = 'Erstellen';
await _openOfferModal();
document.getElementById('offerTemplateInput').value = '';
document.getElementById('offerTemplateValue').value = '';
document.getElementById('offerGenderAll').checked = false;
document.querySelectorAll('input[name="offerGender"]').forEach(cb => { cb.checked = false; cb.disabled = false; });
document.querySelector('input[name="offerStartMode"][value="direct"]').checked = true;
document.getElementById('createOfferModal').style.display = 'flex';
}
async function openEditOfferModal(o) {
_editOfferId = o.id;
document.getElementById('offerModalTitle').textContent = '✏️ Keyholder-Angebot bearbeiten';
document.getElementById('createOfferSubmitBtn').textContent = 'Speichern';
await _openOfferModal();
// Vorlage vorauswählen
const tpl = _offerTemplates.find(t => t.templateId === o.templateId);
if (tpl) {
const badge = tpl.lockType === 'TIMELOCK' ? '⏱' : '🃏';
document.getElementById('offerTemplateInput').value = badge + ' ' + (tpl.name || 'Unbenannte Vorlage');
document.getElementById('offerTemplateValue').value = tpl.templateId;
} else {
// Template nicht mehr in eigenen/abonnierten → Fallback: Name anzeigen
document.getElementById('offerTemplateInput').value = o.templateName || '';
document.getElementById('offerTemplateValue').value = o.templateId;
}
// Geschlechter vorauswählen
const genders = o.targetGenders || [];
const allSelected = genders.length === 0;
document.getElementById('offerGenderAll').checked = allSelected;
document.querySelectorAll('input[name="offerGender"]').forEach(cb => {
cb.checked = !allSelected && genders.includes(cb.value);
cb.disabled = allSelected;
});
// Startmodus
const modeVal = o.directStart ? 'direct' : 'confirm';
document.querySelector(`input[name="offerStartMode"][value="${modeVal}"]`).checked = true;
document.getElementById('createOfferModal').style.display = 'flex';
}
function setupOfferTemplateCombo() {
const input = document.getElementById('offerTemplateInput');
const dropdown = document.getElementById('offerTemplateDropdown');
const hidden = document.getElementById('offerTemplateValue');
// Vorherige Listener entfernen durch Klonen
const newInput = input.cloneNode(true);
input.parentNode.replaceChild(newInput, input);
function renderDropdown(query) {
const q = query.toLowerCase().trim();
const filtered = q
? _offerTemplates.filter(t => (t.name || '').toLowerCase().includes(q))
: _offerTemplates;
dropdown.innerHTML = '';
if (filtered.length === 0) {
dropdown.innerHTML = '<div class="combo-empty">Keine Vorlagen gefunden.</div>';
} else {
filtered.forEach(t => {
const badge = t.lockType === 'TIMELOCK' ? '⏱' : '🃏';
const label = t.name || 'Unbenannte Vorlage';
const div = document.createElement('div');
div.className = 'combo-option';
div.innerHTML = `${badge} ${label}`;
div.addEventListener('mousedown', e => {
e.preventDefault();
hidden.value = t.templateId;
newInput.value = badge + ' ' + label;
dropdown.classList.remove('open');
});
dropdown.appendChild(div);
});
}
dropdown.classList.add('open');
}
newInput.addEventListener('input', () => { hidden.value = ''; renderDropdown(newInput.value); });
newInput.addEventListener('focus', () => renderDropdown(newInput.value));
newInput.addEventListener('blur', () => {
setTimeout(() => {
dropdown.classList.remove('open');
if (!hidden.value) newInput.value = '';
}, 150);
});
}
function closeCreateOfferModal() {
document.getElementById('createOfferModal').style.display = 'none';
_editOfferId = null;
}
function toggleAllGenders(allCb) {
document.querySelectorAll('input[name="offerGender"]').forEach(cb => { cb.checked = false; cb.disabled = allCb.checked; });
}
async function submitCreateOffer() {
const templateId = document.getElementById('offerTemplateValue').value;
if (!templateId) { showOfferError('Bitte eine Vorlage auswählen.'); return; }
const allGenders = document.getElementById('offerGenderAll').checked;
const targetGenders = allGenders
? []
: Array.from(document.querySelectorAll('input[name="offerGender"]:checked')).map(cb => cb.value);
const directStart = document.querySelector('input[name="offerStartMode"]:checked').value === 'direct';
const btn = document.getElementById('createOfferSubmitBtn');
btn.disabled = true;
const url = _editOfferId ? `/keyholder-offers/${_editOfferId}` : '/keyholder-offers';
const method = _editOfferId ? 'PATCH' : 'POST';
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ templateId, targetGenders, directStart })
});
if (res.ok) {
closeCreateOfferModal();
loadOffers();
} else if (res.status === 403) {
const d = await res.json().catch(() => ({}));
showOfferError(d.error === 'offer_limit_reached'
? 'Limit erreicht upgrade auf Premium für unbegrenzte Angebote.'
: 'Keine Berechtigung.');
btn.disabled = false;
} else {
showOfferError(_editOfferId ? 'Fehler beim Speichern.' : 'Fehler beim Erstellen.');
btn.disabled = false;
}
}
function showOfferError(msg) {
const el = document.getElementById('createOfferError');
el.textContent = msg;
el.style.display = '';
}
</script>
<!-- Aufgaben-Auswahl-Popup -->
<div id="ctpModal" onclick="if(event.target===this)closeChooseTaskPopup()" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.55);z-index:1000;align-items:center;justify-content:center;padding:1rem;">
<div style="background:var(--color-card);border:1px solid var(--color-secondary);border-radius:12px;padding:1.5rem;max-width:420px;width:100%;">
<h3 style="margin:0 0 0.25rem;font-size:1rem;">Aufgabe zuweisen</h3>
<div id="ctpTaskTitle" style="font-size:0.95rem;font-weight:600;margin-bottom:0.2rem;"></div>
<div id="ctpTaskMins" style="font-size:0.8rem;color:var(--color-muted);margin-bottom:0.3rem;"></div>
<div id="ctpTaskDesc" style="font-size:0.82rem;color:var(--color-muted);margin-bottom:1rem;line-height:1.4;"></div>
<div style="font-size:0.72rem;font-weight:700;color:var(--color-primary);text-transform:uppercase;letter-spacing:0.06em;margin-bottom:0.4rem;">Strafe bei Ablehnung / Ablauf</div>
<div style="display:flex;flex-direction:column;gap:0.5rem;margin-bottom:1.1rem;">
<label style="display:flex;align-items:center;gap:0.6rem;font-size:0.88rem;cursor:pointer;">
<input type="checkbox" id="ctpPenaltyFreezeEnabled" onchange="toggleCtpFreezeFields()" style="width:auto;flex-shrink:0;">
<span>Einfrieren für …</span>
</label>
<div id="ctpPenaltyFreezeFields" style="padding-left:1.6rem;opacity:0.4;pointer-events:none;transition:opacity 0.15s;">
<div class="time-picker">
<div class="tp-seg">
<div class="tp-seg-row">
<button type="button" onclick="ctpFreezeTpChange(-1,'d')"></button>
<input type="text" id="ctpf_d" value="0" readonly>
<button type="button" onclick="ctpFreezeTpChange(1,'d')">+</button>
</div>
<span class="tp-label">Tage</span>
</div>
<span class="tp-colon">:</span>
<div class="tp-seg">
<div class="tp-seg-row">
<button type="button" onclick="ctpFreezeTpChange(-1,'h')"></button>
<input type="text" id="ctpf_h" value="04" readonly>
<button type="button" onclick="ctpFreezeTpChange(1,'h')">+</button>
</div>
<span class="tp-label">Stunden</span>
</div>
<span class="tp-colon">:</span>
<div class="tp-seg">
<div class="tp-seg-row">
<button type="button" onclick="ctpFreezeTpChange(-1,'m')"></button>
<input type="text" id="ctpf_m" value="00" readonly>
<button type="button" onclick="ctpFreezeTpChange(1,'m')">+</button>
</div>
<span class="tp-label">Minuten</span>
</div>
</div>
</div>
<label style="display:flex;align-items:center;gap:0.6rem;font-size:0.88rem;cursor:pointer;">
<input type="checkbox" id="ctpPenaltyRedEnabled" onchange="toggleCtpRedFields()" style="width:auto;flex-shrink:0;">
<span>Rote Karten hinzufügen:</span>
</label>
<div id="ctpPenaltyRedFields" style="padding-left:1.6rem;display:flex;align-items:center;gap:0.5rem;opacity:0.4;pointer-events:none;transition:opacity 0.15s;">
<button type="button" onclick="ctpRedChange(-1)" style="width:26px;height:26px;background:var(--color-secondary);border:1px solid var(--color-muted);border-radius:5px;cursor:pointer;font-size:1rem;font-weight:700;color:var(--color-text);display:flex;align-items:center;justify-content:center;padding:0;"></button>
<input type="text" id="ctpRedCount" value="1" readonly style="width:30px;text-align:center;background:var(--color-secondary);border:1px solid var(--color-muted);border-radius:4px;color:var(--color-text);font-size:0.95rem;font-weight:600;font-family:monospace;padding:0.18rem 0;">
<button type="button" onclick="ctpRedChange(1)" style="width:26px;height:26px;background:var(--color-secondary);border:1px solid var(--color-muted);border-radius:5px;cursor:pointer;font-size:1rem;font-weight:700;color:var(--color-text);display:flex;align-items:center;justify-content:center;padding:0;">+</button>
<span style="font-size:0.82rem;color:var(--color-muted);">rote Karten</span>
</div>
</div>
<div id="ctpPenaltyError" style="display:none;font-size:0.83rem;color:#e74c3c;margin-bottom:0.5rem;">Bitte wähle mindestens eine Strafe aus.</div>
<div style="display:flex;gap:0.6rem;justify-content:flex-end;">
<button onclick="closeChooseTaskPopup()"
style="padding:0.45rem 1rem;border-radius:6px;border:1px solid var(--color-secondary);background:transparent;color:var(--color-text);cursor:pointer;">
Abbrechen
</button>
<button onclick="confirmChooseTask()"
style="padding:0.45rem 1rem;border-radius:6px;border:none;background:var(--color-primary);color:#fff;cursor:pointer;font-weight:600;">
Aufgabe zuweisen
</button>
</div>
</div>
</div>
</body>
</html>