1750 lines
76 KiB
HTML
1750 lines
76 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>Chastity Game – xXx Sphere</title>
|
||
<link rel="stylesheet" href="/css/variables.css">
|
||
<link rel="stylesheet" href="/css/style.css">
|
||
<style>
|
||
.keyholder-pending-banner {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 0.75rem;
|
||
background: rgba(233,69,96,0.12);
|
||
border: 1px solid rgba(233,69,96,0.35);
|
||
border-radius: 10px;
|
||
padding: 1rem 1.1rem;
|
||
margin-bottom: 1.25rem;
|
||
font-size: 0.9rem;
|
||
color: var(--color-text);
|
||
line-height: 1.5;
|
||
}
|
||
.keyholder-pending-banner .icon { font-size: 1.4rem; flex-shrink: 0; }
|
||
|
||
/* ── Karten-Anzeige ── */
|
||
.cards-panel {
|
||
background: var(--color-card);
|
||
border: 1px solid var(--color-secondary);
|
||
border-radius: 10px;
|
||
padding: 1rem 1.1rem;
|
||
margin-bottom: 1.25rem;
|
||
}
|
||
.cards-panel-title {
|
||
font-size: 0.75rem;
|
||
font-weight: 700;
|
||
color: var(--color-muted);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.07em;
|
||
margin-bottom: 0.75rem;
|
||
}
|
||
/* ── Nächste Karte Panel ── */
|
||
.nextcard-panel {
|
||
background: var(--color-card);
|
||
border: 1px solid var(--color-secondary);
|
||
border-radius: 10px;
|
||
padding: 1rem 1.1rem;
|
||
margin-bottom: 1.25rem;
|
||
}
|
||
.nextcard-panel-title {
|
||
font-size: 0.75rem;
|
||
font-weight: 700;
|
||
color: var(--color-muted);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.07em;
|
||
margin-bottom: 0.75rem;
|
||
}
|
||
.nextcard-cards {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 0.6rem;
|
||
position: relative;
|
||
border-radius: 6px;
|
||
padding: 0.75rem 0.5rem 0.5rem;
|
||
overflow: visible;
|
||
}
|
||
.nextcard-card-img {
|
||
width: calc((100% - 5 * 0.6rem) / 6);
|
||
height: auto;
|
||
border-radius: 6px;
|
||
position: relative;
|
||
z-index: 1;
|
||
transition: transform 0.15s, box-shadow 0.15s;
|
||
}
|
||
.nextcard-panel.drawable .nextcard-card-img:hover {
|
||
transform: translateY(-8px) scale(1.06);
|
||
box-shadow: 0 8px 20px rgba(0,0,0,0.4);
|
||
z-index: 10;
|
||
cursor: pointer;
|
||
}
|
||
.nextcard-overlay {
|
||
position: absolute;
|
||
inset: 0;
|
||
background: rgba(0,0,0,0.58);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 0.3rem;
|
||
z-index: 5;
|
||
border-radius: 6px;
|
||
}
|
||
.nextcard-timer-box {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 0.3rem;
|
||
background: rgba(0,0,0,0.45);
|
||
border: 1px solid rgba(255,255,255,0.35);
|
||
border-radius: 10px;
|
||
padding: 0.55rem 1.1rem;
|
||
}
|
||
.nextcard-overlay-label {
|
||
font-size: 0.72rem;
|
||
font-weight: 700;
|
||
color: rgba(255,255,255,0.65);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.07em;
|
||
}
|
||
.nextcard-countdown {
|
||
font-size: 1.5rem;
|
||
font-weight: 700;
|
||
font-family: monospace;
|
||
color: #fff;
|
||
}
|
||
|
||
/* ── Frost-Overlay (Eis) – gemeinsam für frozen + task ── */
|
||
.nextcard-overlay.frozen,
|
||
.nextcard-overlay.task {
|
||
border-radius: 6px;
|
||
background:
|
||
radial-gradient(ellipse at 18% 28%, rgba(255,255,255,0.38) 0%, transparent 48%),
|
||
radial-gradient(ellipse at 82% 72%, rgba(255,255,255,0.28) 0%, transparent 42%),
|
||
radial-gradient(ellipse at 55% 12%, rgba(255,255,255,0.22) 0%, transparent 38%),
|
||
rgba(55,140,210,0.68);
|
||
background-size: auto, auto, auto, 48px 48px;
|
||
}
|
||
/* ── Task-Overlay ── */
|
||
.nextcard-task-box {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 0.6rem;
|
||
background: var(--color-card);
|
||
border: 1px solid var(--color-secondary);
|
||
border-radius: 10px;
|
||
padding: 0.9rem 1.2rem;
|
||
max-width: 85%;
|
||
text-align: center;
|
||
}
|
||
.nextcard-task-text {
|
||
font-size: 0.92rem;
|
||
color: var(--color-text);
|
||
line-height: 1.5;
|
||
}
|
||
.nextcard-task-countdown {
|
||
font-size: 1.1rem;
|
||
font-weight: 700;
|
||
font-family: monospace;
|
||
color: var(--color-primary);
|
||
}
|
||
.btn-task-erledigt {
|
||
padding: 0.45rem 1.2rem;
|
||
border-radius: 8px;
|
||
border: none;
|
||
font-size: 0.9rem;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
background: #27ae60;
|
||
color: #fff;
|
||
transition: background 0.15s, opacity 0.15s;
|
||
width: auto;
|
||
}
|
||
.btn-task-erledigt:disabled {
|
||
background: rgba(255,255,255,0.2);
|
||
color: rgba(255,255,255,0.45);
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
/* ── Hygiene-Panel ── */
|
||
.hygiene-panel {
|
||
background: var(--color-card);
|
||
border: 1px solid var(--color-secondary);
|
||
border-radius: 10px;
|
||
padding: 1rem 1.1rem;
|
||
margin-bottom: 1.25rem;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 1rem;
|
||
}
|
||
.hygiene-panel-title {
|
||
font-size: 0.75rem;
|
||
font-weight: 700;
|
||
color: var(--color-muted);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.07em;
|
||
margin-bottom: 0.25rem;
|
||
}
|
||
.hygiene-countdown {
|
||
font-size: 1.4rem;
|
||
font-weight: 700;
|
||
font-family: monospace;
|
||
color: var(--color-text);
|
||
}
|
||
.btn-hygiene {
|
||
white-space: nowrap;
|
||
width: auto;
|
||
padding: 0.5rem 1.1rem;
|
||
font-size: 0.9rem;
|
||
transition: opacity 0.2s, background 0.2s;
|
||
}
|
||
.btn-hygiene:disabled {
|
||
opacity: 0.4;
|
||
cursor: not-allowed;
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* ── Verifikations-Panel ── */
|
||
.verification-panel {
|
||
background: var(--color-card);
|
||
border: 1px solid var(--color-secondary);
|
||
border-radius: 10px;
|
||
padding: 1rem 1.1rem;
|
||
margin-bottom: 1.25rem;
|
||
}
|
||
.verification-panel-title {
|
||
font-size: 0.75rem;
|
||
font-weight: 700;
|
||
color: var(--color-muted);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.07em;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
.verification-panel-body {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 1rem;
|
||
}
|
||
.verification-due-text {
|
||
font-size: 0.95rem;
|
||
color: var(--color-text);
|
||
}
|
||
.btn-verification {
|
||
white-space: nowrap;
|
||
width: auto;
|
||
padding: 0.5rem 1.1rem;
|
||
font-size: 0.9rem;
|
||
}
|
||
.verification-code-label {
|
||
font-size: 0.85rem;
|
||
color: var(--color-muted);
|
||
margin-bottom: 0.35rem;
|
||
}
|
||
.verification-code-display {
|
||
font-size: 2rem;
|
||
font-weight: 700;
|
||
font-family: monospace;
|
||
letter-spacing: 0.25em;
|
||
color: var(--color-primary);
|
||
background: rgba(233,69,96,0.08);
|
||
border: 1px solid rgba(233,69,96,0.25);
|
||
border-radius: 8px;
|
||
padding: 0.5rem 1rem;
|
||
margin-bottom: 0.5rem;
|
||
text-align: center;
|
||
}
|
||
.verification-upload-label {
|
||
display: block;
|
||
cursor: pointer;
|
||
background: var(--color-secondary);
|
||
border: 1px dashed var(--color-muted);
|
||
border-radius: 8px;
|
||
padding: 0.75rem 1rem;
|
||
text-align: center;
|
||
font-size: 0.9rem;
|
||
color: var(--color-text);
|
||
transition: background 0.2s;
|
||
}
|
||
.verification-upload-label:hover { background: var(--color-hover); }
|
||
.verification-upload-label input[type="file"] { display: none; }
|
||
.verification-done-body {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 1rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
.verification-done-check {
|
||
font-size: 0.95rem;
|
||
color: #2ecc71;
|
||
font-weight: 600;
|
||
}
|
||
.verification-done-check::before { content: '✓ '; }
|
||
.verification-votes {
|
||
display: flex;
|
||
gap: 1rem;
|
||
font-size: 0.9rem;
|
||
color: var(--color-muted);
|
||
}
|
||
.vote-up { color: #2ecc71; font-weight: 600; }
|
||
.vote-down { color: #e74c3c; font-weight: 600; }
|
||
|
||
/* ── Karte ziehen Dialog ── */
|
||
.draw-modal-backdrop {
|
||
display: none;
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0,0,0,0.65);
|
||
z-index: 500;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.draw-modal-backdrop.open { display: flex; }
|
||
.draw-modal-box {
|
||
background: var(--color-card);
|
||
border: 1px solid var(--color-secondary);
|
||
border-radius: 14px;
|
||
padding: 2rem 1.75rem 1.5rem;
|
||
max-width: 340px;
|
||
width: 90%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
}
|
||
|
||
/* Flip-Karte */
|
||
.flip-card {
|
||
perspective: 900px;
|
||
width: 130px;
|
||
height: 195px;
|
||
}
|
||
.flip-card-inner {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 100%;
|
||
transition: transform 0.65s cubic-bezier(.4,0,.2,1);
|
||
transform-style: preserve-3d;
|
||
}
|
||
.flip-card-inner.flipped { transform: rotateY(180deg); }
|
||
.flip-card-front,
|
||
.flip-card-back {
|
||
position: absolute;
|
||
inset: 0;
|
||
backface-visibility: hidden;
|
||
}
|
||
.flip-card-front img,
|
||
.flip-card-back img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
border-radius: 8px;
|
||
box-shadow: 0 6px 20px rgba(0,0,0,0.4);
|
||
}
|
||
.flip-card-back { transform: rotateY(180deg); }
|
||
|
||
/* Karten-Info unter der Karte */
|
||
.draw-card-info {
|
||
text-align: center;
|
||
opacity: 0;
|
||
transition: opacity 0.3s 0.2s;
|
||
}
|
||
.draw-card-info.visible { opacity: 1; }
|
||
.draw-card-info h3 { margin: 0 0 0.35rem; font-size: 1.05rem; }
|
||
.draw-card-info p { margin: 0; font-size: 0.85rem; color: var(--color-muted); line-height: 1.5; }
|
||
|
||
/* Grüne-Karte Entscheidung */
|
||
.draw-green-choice {
|
||
display: none;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 0.6rem;
|
||
width: 100%;
|
||
}
|
||
.draw-green-choice.visible { display: flex; }
|
||
.draw-unlock-code {
|
||
font-size: 2rem;
|
||
font-weight: 700;
|
||
font-family: monospace;
|
||
letter-spacing: 0.15em;
|
||
color: var(--color-primary);
|
||
text-align: center;
|
||
display: none;
|
||
}
|
||
|
||
/* Buttons im Dialog */
|
||
.draw-modal-actions {
|
||
display: flex;
|
||
gap: 0.6rem;
|
||
justify-content: center;
|
||
width: 100%;
|
||
flex-wrap: wrap;
|
||
}
|
||
.draw-modal-actions button {
|
||
padding: 0.55rem 1.2rem;
|
||
border-radius: 8px;
|
||
border: none;
|
||
cursor: pointer;
|
||
font-size: 0.9rem;
|
||
font-weight: 600;
|
||
width: auto;
|
||
}
|
||
.btn-draw-ok {
|
||
background: var(--color-primary);
|
||
color: #fff;
|
||
}
|
||
.btn-draw-unlock {
|
||
background: #27ae60;
|
||
color: #fff;
|
||
}
|
||
.btn-draw-keep {
|
||
background: none;
|
||
border: 1px solid var(--color-secondary) !important;
|
||
color: var(--color-muted);
|
||
}
|
||
|
||
/* ── Hygiene-Öffnung Modal ── */
|
||
.hygiene-modal-backdrop {
|
||
display: none;
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0,0,0,0.65);
|
||
z-index: 500;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.hygiene-modal-backdrop.open { display: flex; }
|
||
.hygiene-modal-box {
|
||
background: var(--color-card);
|
||
border: 1px solid var(--color-secondary);
|
||
border-radius: 14px;
|
||
padding: 2rem 1.75rem 1.5rem;
|
||
max-width: 360px;
|
||
width: 90%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
text-align: center;
|
||
}
|
||
.hygiene-modal-icon { font-size: 2.5rem; }
|
||
.hygiene-modal-box h3 { margin: 0; font-size: 1.05rem; }
|
||
.hygiene-unlock-code {
|
||
font-size: 2rem;
|
||
font-weight: 700;
|
||
font-family: monospace;
|
||
letter-spacing: 0.15em;
|
||
color: var(--color-primary);
|
||
background: var(--color-secondary);
|
||
border-radius: 8px;
|
||
padding: 0.5rem 1.2rem;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
.hygiene-timer {
|
||
font-size: 1.6rem;
|
||
font-weight: 700;
|
||
font-family: monospace;
|
||
color: var(--color-text);
|
||
transition: color 0.3s;
|
||
}
|
||
.hygiene-timer.overtime { color: #e74c3c; }
|
||
.hygiene-timer-label {
|
||
font-size: 0.75rem;
|
||
color: var(--color-muted);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.06em;
|
||
margin-top: -0.5rem;
|
||
}
|
||
.hygiene-new-code-section { width: 100%; }
|
||
.hygiene-new-code-label {
|
||
font-size: 0.75rem;
|
||
color: var(--color-muted);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.06em;
|
||
margin-bottom: 0.4rem;
|
||
}
|
||
|
||
/* ── Lock beenden Button ── */
|
||
.btn-lock-beenden {
|
||
background: transparent;
|
||
border: 1px solid rgba(200,50,50,0.45);
|
||
color: rgba(200,50,50,0.7);
|
||
font-size: 0.82rem;
|
||
padding: 0.5rem 1rem;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
margin-top: 1.5rem;
|
||
width: auto;
|
||
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||
}
|
||
.btn-lock-beenden:hover {
|
||
background: rgba(200,50,50,0.12);
|
||
color: #e74c3c;
|
||
border-color: #e74c3c;
|
||
}
|
||
|
||
/* ── Warn-Modal ── */
|
||
.warn-modal-backdrop {
|
||
display: none;
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0,0,0,0.6);
|
||
z-index: 500;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.warn-modal-backdrop.open { display: flex; }
|
||
.warn-modal-box {
|
||
background: var(--color-card);
|
||
border: 1px solid var(--color-secondary);
|
||
border-radius: 12px;
|
||
padding: 2rem 1.75rem 1.5rem;
|
||
max-width: 360px;
|
||
width: 90%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.75rem;
|
||
}
|
||
.warn-modal-box h3 { margin: 0; font-size: 1.05rem; }
|
||
.warn-modal-box p { margin: 0; font-size: 0.88rem; color: var(--color-muted); line-height: 1.55; }
|
||
.warn-modal-actions { display: flex; gap: 0.6rem; justify-content: flex-end; margin-top: 0.25rem; }
|
||
.warn-modal-actions .btn-cancel {
|
||
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;
|
||
}
|
||
.warn-modal-actions .btn-danger {
|
||
background: #c0392b;
|
||
border: none;
|
||
color: #fff;
|
||
padding: 0.5rem 1.1rem;
|
||
border-radius: 7px;
|
||
cursor: pointer;
|
||
font-size: 0.88rem;
|
||
width: auto;
|
||
}
|
||
.warn-modal-actions .btn-danger:hover { background: #e74c3c; }
|
||
</style>
|
||
</head>
|
||
<body class="app">
|
||
<div class="main">
|
||
<div class="content">
|
||
|
||
<h2 style="margin-bottom:1.25rem;">🔒 Chastity Session</h2>
|
||
|
||
<div id="keyholderInfoBar" style="display:none;align-items:center;gap:0.75rem;background:var(--color-card);border:1px solid var(--color-secondary);border-radius:10px;padding:0.65rem 1rem;margin-bottom:1rem;">
|
||
<div id="keyholderInfoAvatar" style="width:42px;height:42px;border-radius:50%;background:var(--color-secondary);display:flex;align-items:center;justify-content:center;font-size:1.2rem;flex-shrink:0;overflow:hidden;border:1px solid rgba(255,255,255,0.08);"></div>
|
||
<div style="font-size:0.88rem;color:var(--color-muted);">Keyholder*In: <a id="keyholderInfoName" href="#" style="font-weight:700;color:var(--color-text);text-decoration:none;"></a></div>
|
||
</div>
|
||
|
||
<!-- Zugewiesene Aufgaben vom Keyholder -->
|
||
<div id="assignedTasksArea" style="display:none;margin-bottom:1rem;"></div>
|
||
|
||
<div class="keyholder-pending-banner" id="keyholderPendingBanner" style="display:none;">
|
||
<span class="icon">⏳</span>
|
||
<div>
|
||
<strong>Keyholder*In-Bestätigung ausstehend</strong><br>
|
||
Die eingetragene Keyholder*In wurde per Direktnachricht eingeladen und muss die Rolle noch bestätigen.
|
||
Bis zur Bestätigung läuft das Lock als Self-Lock.
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Karten-Panel (nur bei showRemainingCards) -->
|
||
<div class="cards-panel" id="cardsPanel" style="display:none;">
|
||
<div class="cards-panel-title">Verbleibende Karten</div>
|
||
<div id="cardsDisplay"></div>
|
||
</div>
|
||
|
||
<!-- Nächste Karte Panel -->
|
||
<div class="nextcard-panel" id="nextcardPanel" style="display:none;">
|
||
<div class="nextcard-panel-title">Nächste Karte</div>
|
||
<div class="nextcard-cards" id="nextcardCards">
|
||
<div class="nextcard-overlay" id="nextcardOverlay" style="display:none;">
|
||
<!-- Normaler Timer / Frozen -->
|
||
<div class="nextcard-timer-box" id="nextcardTimerBox">
|
||
<div class="nextcard-overlay-label" id="nextcardOverlayLabel">Nächste Karte ziehbar in</div>
|
||
<div class="nextcard-countdown" id="nextcardCountdown" style="display:none;">–</div>
|
||
<div class="frozen-label" id="nextcardFrozenLabel" style="display:none;">❄️ Eingefroren</div>
|
||
</div>
|
||
<!-- Task -->
|
||
<div class="nextcard-task-box" id="nextcardTaskBox" style="display:none;">
|
||
<div class="nextcard-task-text" id="nextcardTaskText"></div>
|
||
<div id="nextcardTaskDescription" style="font-size:0.82rem;color:rgba(255,255,255,0.7);margin-top:0.25rem;line-height:1.4;display:none;"></div>
|
||
<div class="nextcard-task-countdown" id="nextcardTaskCountdown" style="display:none;"></div>
|
||
<button class="btn-task-erledigt" id="btnTaskErledigt" disabled onclick="taskErledigt()">✓ Erledigt</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Hygiene-Panel -->
|
||
<div class="hygiene-panel" id="hygienePanel" style="display:none;">
|
||
<div>
|
||
<div class="hygiene-panel-title">Nächste Hygiene-Öffnung</div>
|
||
<div class="hygiene-countdown" id="hygieneCountdown">–</div>
|
||
</div>
|
||
<button class="btn-hygiene" id="hygieneBtn" style="display:none;" onclick="openHygieneModal()">🚿 Hygiene-Öffnung</button>
|
||
</div>
|
||
|
||
<!-- Verifikations-Panel -->
|
||
<div class="verification-panel" id="verificationPanel" style="display:none;">
|
||
<div class="verification-panel-title">Tägliche Verifikation</div>
|
||
<!-- Status: ausstehend (noch nicht gestartet) -->
|
||
<div class="verification-panel-body" id="verificationDueBody">
|
||
<span class="verification-due-text">Heute noch nicht verifiziert</span>
|
||
<button class="btn-verification" onclick="startVerification()">📷 Verifikation starten</button>
|
||
</div>
|
||
<!-- Status: gestartet, Bild fehlt noch -->
|
||
<div class="verification-panel-body" id="verificationPendingBody" style="display:none;">
|
||
<span class="verification-due-text">Verifikation gestartet – Bild noch ausstehend</span>
|
||
<button class="btn-verification" onclick="startVerification()">📷 Fortsetzen</button>
|
||
</div>
|
||
<!-- Status: abgeschlossen -->
|
||
<div class="verification-done-body" id="verificationDoneBody" style="display:none;">
|
||
<div>
|
||
<div class="verification-done-check">Heute bereits verifiziert</div>
|
||
<div class="verification-votes" id="verificationVotes"></div>
|
||
</div>
|
||
<button class="btn-verification" id="btnVerificationRenew" style="display:none;" onclick="renewVerification()">🔄 Erneuern</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="lockContent" style="color:var(--color-muted);font-size:0.95rem;">
|
||
Wird geladen…
|
||
</div>
|
||
|
||
<div id="lockActionArea" style="display:flex; justify-content:flex-end; margin-top:2rem;"></div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Verifikations-Modal -->
|
||
<div class="hygiene-modal-backdrop" id="verificationModal" style="display:none;">
|
||
<div class="hygiene-modal-box">
|
||
<div class="hygiene-modal-icon">📷</div>
|
||
<div class="hygiene-modal-title">Tägliche Verifikation</div>
|
||
<div class="verification-code-label">Dein Verifikationscode:</div>
|
||
<div class="verification-code-display" id="verificationCode">–</div>
|
||
<p style="font-size:0.88rem;color:var(--color-muted);margin:0.5rem 0 1rem;">
|
||
Fotografiere dich mit diesem Code gut sichtbar und lade das Bild hoch.
|
||
</p>
|
||
<label class="verification-upload-label">
|
||
<input type="file" id="verificationImageInput" accept="image/*" capture="user" onchange="onVerificationImageSelected(this)">
|
||
<span id="verificationUploadText">📁 Bild auswählen / aufnehmen</span>
|
||
</label>
|
||
<div id="verificationPreview" style="display:none;margin-top:0.75rem;">
|
||
<img id="verificationPreviewImg" style="max-width:100%;max-height:200px;border-radius:8px;object-fit:contain;">
|
||
</div>
|
||
<div style="display:flex;gap:0.75rem;margin-top:1.25rem;justify-content:flex-end;">
|
||
<button class="btn-modal-secondary" onclick="closeVerificationModal()">Abbrechen</button>
|
||
<button class="btn-modal-primary" id="verificationDoneBtn" disabled onclick="completeVerification()">✓ Fertig</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Hygiene-Öffnung-Modal -->
|
||
<div class="hygiene-modal-backdrop" id="hygieneModal">
|
||
<div class="hygiene-modal-box">
|
||
<div class="hygiene-modal-icon">🚿</div>
|
||
<h3>Hygiene-Öffnung</h3>
|
||
|
||
<!-- Phase 1: Öffnung läuft -->
|
||
<div id="hygienePhase1" style="width:100%;display:flex;flex-direction:column;align-items:center;gap:0.75rem;">
|
||
<p style="margin:0;font-size:0.88rem;color:var(--color-muted);">Dein aktueller Entsperrcode:</p>
|
||
<div class="hygiene-unlock-code" id="hygieneCurrentCode">–</div>
|
||
<div style="margin-top:0.5rem;display:flex;flex-direction:column;align-items:center;gap:0.2rem;">
|
||
<div class="hygiene-timer-label" id="hygieneTimerLabel">Verbleibende Zeit</div>
|
||
<div class="hygiene-timer" id="hygieneTimer">–</div>
|
||
</div>
|
||
<button class="btn-draw-ok" style="margin-top:0.25rem;" onclick="endHygieneOpening()">✓ Beenden</button>
|
||
</div>
|
||
|
||
<!-- Phase 2: Neuer Code + Vergessen-Flow -->
|
||
<div id="hygienePhase2" style="display:none;width:100%;flex-direction:column;align-items:center;gap:0.75rem;">
|
||
<p id="hygienePhase2Hint" style="margin:0;font-size:0.88rem;color:var(--color-muted);">Dein neuer Code zum Abschließen:</p>
|
||
<div class="hygiene-unlock-code" id="hygieneNewCode">–</div>
|
||
<div id="hygieneScrambleCountdown" style="display:none;font-size:0.82rem;color:var(--color-muted);font-family:monospace;"></div>
|
||
<button class="btn-draw-ok" id="hygienePhase2Btn" onclick="startHygieneScramble()">OK</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Temp-Öffnung-Modal (CARD / TASK / etc.) -->
|
||
<div class="hygiene-modal-backdrop" id="tempOpeningModal">
|
||
<div class="hygiene-modal-box">
|
||
<div class="hygiene-modal-icon">🔓</div>
|
||
<h3>Lock geöffnet</h3>
|
||
|
||
<!-- Phase 1: Code anzeigen -->
|
||
<div id="tempPhase1" style="width:100%;display:flex;flex-direction:column;align-items:center;gap:0.75rem;">
|
||
<p style="margin:0;font-size:0.88rem;color:var(--color-muted);">Dein aktueller Entsperrcode:</p>
|
||
<div class="hygiene-unlock-code" id="tempOpeningCode">–</div>
|
||
<button class="btn-draw-ok" style="margin-top:0.25rem;" onclick="endTempOpeningFlow()">🔒 Öffnung beenden</button>
|
||
</div>
|
||
|
||
<!-- Phase 2: Neuer Code (nur UNLOCK_CODE) -->
|
||
<div id="tempPhase2" style="display:none;width:100%;flex-direction:column;align-items:center;gap:0.75rem;">
|
||
<p id="tempPhase2Hint" style="margin:0;font-size:0.88rem;color:var(--color-muted);">Dein neuer Code zum Abschließen:</p>
|
||
<div class="hygiene-unlock-code" id="tempNewCode">–</div>
|
||
<div id="tempScrambleCountdown" style="display:none;font-size:0.82rem;color:var(--color-muted);font-family:monospace;"></div>
|
||
<button class="btn-draw-ok" id="tempPhase2Btn" onclick="startTempScramble()">OK</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Karte-Ziehen-Modal -->
|
||
<div class="draw-modal-backdrop" id="drawModal">
|
||
<div class="draw-modal-box">
|
||
<div class="flip-card">
|
||
<div class="flip-card-inner" id="flipCardInner">
|
||
<div class="flip-card-front">
|
||
<img src="/img/card.png" alt="Karte">
|
||
</div>
|
||
<div class="flip-card-back">
|
||
<img id="drawnCardImg" src="" alt="">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="draw-card-info" id="drawCardInfo">
|
||
<h3 id="drawCardName"></h3>
|
||
<p id="drawCardDesc"></p>
|
||
</div>
|
||
|
||
<div id="drawTaskPendingHint" style="display:none;margin-top:0.75rem;padding:0.75rem 1rem;border-radius:8px;background:rgba(233,69,96,0.10);border:1px solid rgba(233,69,96,0.3);font-size:0.88rem;color:var(--color-text);line-height:1.5;text-align:center;">
|
||
<span id="drawTaskPendingText"></span>
|
||
</div>
|
||
|
||
<!-- Grüne Karte: Entscheidung -->
|
||
<div class="draw-green-choice" id="drawGreenChoice">
|
||
<p id="drawGreenText" style="text-align:center;font-size:0.88rem;color:var(--color-muted);margin:0;">
|
||
Du hast die grüne Karte gezogen!<br>Möchtest du den Entsperrcode erhalten und die Session beenden,<br>oder die Karte zurücklegen?
|
||
</p>
|
||
<div class="draw-unlock-code" id="drawUnlockCode"></div>
|
||
</div>
|
||
|
||
<div class="draw-modal-actions" id="drawModalActions" style="display:none;">
|
||
<!-- Non-green: OK -->
|
||
<button class="btn-draw-ok" id="btnDrawOk" onclick="closeDrawModal()">OK</button>
|
||
<!-- Green: zwei Optionen -->
|
||
<button class="btn-draw-unlock" id="btnDrawUnlock" style="display:none;" onclick="confirmUnlock()">🔓 Entsperren</button>
|
||
<button class="btn-draw-keep" id="btnDrawKeep" style="display:none;" onclick="keepGreenCard()">Zurücklegen</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TTLock Lade-Dialog -->
|
||
<div class="warn-modal-backdrop" id="ttlLoadingModal">
|
||
<div class="warn-modal-box" style="text-align:center;padding:2rem 1.5rem;">
|
||
<div style="font-size:2rem;margin-bottom:0.75rem;">⏳</div>
|
||
<div style="font-weight:600;margin-bottom:0.4rem;">TTLock-Kommunikation läuft…</div>
|
||
<div style="font-size:0.85rem;color:var(--color-muted);">Bitte warten, der TTLock-Server wird kontaktiert.</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Warn-Modal (TestLock beenden) -->
|
||
<div class="warn-modal-backdrop" id="warnModal">
|
||
<div class="warn-modal-box">
|
||
<h3>Lock beenden?</h3>
|
||
<p>Dein Entsperrcode:</p>
|
||
<div id="warnModalUnlockCode" style="font-size:1.6rem;font-weight:700;letter-spacing:0.15em;text-align:center;margin:0.5rem 0 1rem;color:var(--color-primary);"></div>
|
||
<div class="warn-modal-actions">
|
||
<button class="btn-cancel" onclick="closeWarnModal()">Abbrechen</button>
|
||
<button class="btn-danger" onclick="lockLoeschen()">Lock beenden</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Notfall-Modal -->
|
||
<div class="warn-modal-backdrop" id="emergencyModal">
|
||
<div class="warn-modal-box">
|
||
<h3>🆘 Notfall-Entsperrung</h3>
|
||
<div id="emergencyModalContent"></div>
|
||
<div class="warn-modal-actions" id="emergencyModalActions">
|
||
<button class="btn-cancel" onclick="closeEmergencyModal()">Abbrechen</button>
|
||
<button class="btn-danger" id="btnEmergencyConfirm" onclick="confirmEmergency()">Notfall bestätigen</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Annehmen/Ablehnen-Dialog für Keyholder-Aufgaben -->
|
||
<div id="assignedTaskModal" 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:92%;display:flex;flex-direction:column;gap:1rem;position:relative;">
|
||
<button onclick="closeAssignedTaskModal()" 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 gestellt</h3>
|
||
<div id="assignedTaskModalInfo" style="font-size:0.88rem;color:var(--color-muted);line-height:1.6;"></div>
|
||
<div id="assignedTaskPenaltyInfo" style="background:rgba(231,76,60,0.08);border:1px solid rgba(231,76,60,0.25);border-radius:8px;padding:0.65rem 0.85rem;font-size:0.83rem;color:#e74c3c;line-height:1.5;"></div>
|
||
<div id="assignedTaskModalError" style="display:none;font-size:0.85rem;color:#e74c3c;"></div>
|
||
<div style="display:flex;gap:0.6rem;justify-content:flex-end;">
|
||
<button onclick="respondAssignedTask('decline')" style="background:rgba(231,76,60,0.12);border:1px solid rgba(231,76,60,0.35);color:#e74c3c;padding:0.5rem 1.1rem;border-radius:7px;cursor:pointer;font-size:0.88rem;font-weight:600;width:auto;">✕ Ablehnen</button>
|
||
<button onclick="respondAssignedTask('accept')" style="background:rgba(46,204,113,0.15);border:1px solid rgba(46,204,113,0.4);color:#2ecc71;padding:0.5rem 1.1rem;border-radius:7px;cursor:pointer;font-size:0.88rem;font-weight:600;width:auto;">✓ Annehmen</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="/js/shared.js"></script>
|
||
<script src="/js/card-defs.js"></script>
|
||
<script src="/js/card-display.js"></script>
|
||
<script src="/js/icons.js"></script>
|
||
<script src="/js/sidebar.js"></script>
|
||
<script src="/js/social-sidebar.js"></script>
|
||
<script>
|
||
|
||
const params = new URLSearchParams(window.location.search);
|
||
const lockId = params.get('lockId');
|
||
|
||
fetch('/login/me').then(r => r.ok ? r.json() : null).then(user => {
|
||
if (!user) { window.location.href = '/login.html'; return; }
|
||
if (lockId) loadLock();
|
||
else document.getElementById('lockContent').textContent = 'Kein Lock angegeben.';
|
||
});
|
||
|
||
async function loadLock() {
|
||
const res = await fetch('/keyholder/cardlock/' + lockId);
|
||
if (res.status === 404) {
|
||
// Prüfen, ob es ein TimeLock ist
|
||
const tlRes = await fetch('/keyholder/timelock/' + lockId);
|
||
if (tlRes.ok) {
|
||
window.location.replace('/games/chastity/activetimelock.html?lockId=' + lockId);
|
||
return;
|
||
}
|
||
document.getElementById('lockContent').textContent = 'Lock nicht gefunden.';
|
||
return;
|
||
}
|
||
if (!res.ok) {
|
||
document.getElementById('lockContent').textContent = 'Lock nicht gefunden.';
|
||
return;
|
||
}
|
||
const lock = await res.json();
|
||
_currentLock = lock;
|
||
document.getElementById('lockContent').textContent = '';
|
||
document.getElementById('keyholderPendingBanner').style.display =
|
||
lock.keyholderInvitationPending ? '' : 'none';
|
||
renderKeyholderBar(lock);
|
||
renderAssignedTasks(lock);
|
||
renderNextCardPanel(lock);
|
||
renderHygienePanel(lock);
|
||
renderVerificationPanel(lock);
|
||
renderTempOpeningPanel(lock);
|
||
renderCardsPanel(lock);
|
||
|
||
if (lock.keyholderRequestedUnlock) {
|
||
showKeyholderUnlockModal(lock.unlockCode || '');
|
||
}
|
||
renderLockActionArea(lock);
|
||
}
|
||
|
||
function showKeyholderUnlockModal(unlockCode) {
|
||
const modal = document.getElementById('drawModal');
|
||
const inner = document.getElementById('flipCardInner');
|
||
const info = document.getElementById('drawCardInfo');
|
||
const green = document.getElementById('drawGreenChoice');
|
||
const actions = document.getElementById('drawModalActions');
|
||
const hint = document.getElementById('drawTaskPendingHint');
|
||
|
||
// Direkt aufgedeckt mit grüner Karte zeigen
|
||
inner.classList.remove('flipped');
|
||
info.classList.remove('visible');
|
||
green.classList.remove('visible');
|
||
hint.style.display = 'none';
|
||
document.getElementById('drawUnlockCode').style.display = 'none';
|
||
document.getElementById('btnDrawOk').style.display = 'none';
|
||
document.getElementById('btnDrawUnlock').style.display = 'none';
|
||
document.getElementById('btnDrawKeep').style.display = 'none';
|
||
actions.style.display = 'none';
|
||
drawnUnlockCode = unlockCode;
|
||
modal.classList.add('open');
|
||
|
||
const def = CARD_LABELS['GREEN'];
|
||
setTimeout(() => {
|
||
document.getElementById('drawnCardImg').src = def.img;
|
||
document.getElementById('drawnCardImg').alt = def.name;
|
||
inner.classList.add('flipped');
|
||
setTimeout(() => {
|
||
const khName = _currentLock && _currentLock.keyholderName ? _currentLock.keyholderName : null;
|
||
document.getElementById('drawCardName').textContent = def.name;
|
||
document.getElementById('drawCardDesc').textContent = khName
|
||
? `🔑 ${khName} hat das Lock freigegeben.`
|
||
: '🔓 Das Lock wird freigegeben.';
|
||
document.getElementById('drawGreenText').style.display = 'none';
|
||
info.classList.add('visible');
|
||
actions.style.display = '';
|
||
green.classList.add('visible');
|
||
document.getElementById('btnDrawUnlock').style.display = '';
|
||
document.getElementById('btnDrawKeep').style.display = 'none';
|
||
}, 700);
|
||
}, 800);
|
||
}
|
||
|
||
let _currentLock = null;
|
||
|
||
function renderLockActionArea(lock) {
|
||
const area = document.getElementById('lockActionArea');
|
||
if (!area) return;
|
||
if (lock.testLock) {
|
||
area.innerHTML = `<button class="btn-lock-beenden" onclick="lockBeendenFragen()">🔓 Lock beenden</button>`;
|
||
} else if (lock.emergencyUnlockRequested) {
|
||
const emergencyMsg = lock.keyholderName
|
||
? `⏳ Notfall-Entsperrung angefordert – ${lock.keyholderName} wurde benachrichtigt.`
|
||
: `⏳ Notfall-Entsperrung angefordert – Das Lock öffnet sich automatisch.`;
|
||
area.innerHTML = `<div style="font-size:0.85rem;color:var(--color-muted);padding:0.5rem 0.75rem;border:1px solid rgba(231,76,60,0.3);border-radius:8px;background:rgba(231,76,60,0.06);">
|
||
${emergencyMsg}
|
||
</div>`;
|
||
} else {
|
||
area.innerHTML = `<button onclick="openEmergencyModal()"
|
||
style="background:rgba(231,76,60,0.1);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;">
|
||
🆘 Notfall
|
||
</button>`;
|
||
}
|
||
}
|
||
|
||
function openEmergencyModal() {
|
||
const hasKH = _currentLock && _currentLock.keyholderName;
|
||
const khName = hasKH ? _currentLock.keyholderName : null;
|
||
const text = hasKH
|
||
? `Im Notfall kannst du eine sofortige Freigabe anfordern.<br>
|
||
<strong style="color:var(--color-text);">${khName}</strong> wird benachrichtigt und hat <strong>1 Stunde</strong> Zeit zu reagieren.
|
||
Reagiert ${khName} nicht, öffnet sich das Lock automatisch.`
|
||
: `Im Notfall kannst du eine sofortige Freigabe anfordern.<br>
|
||
Da kein Keyholder zugewiesen ist, öffnet sich das Lock <strong>sofort</strong>.`;
|
||
document.getElementById('emergencyModalContent').innerHTML = `
|
||
<p style="font-size:0.88rem;color:var(--color-muted);line-height:1.5;margin:0 0 0.5rem;">${text}</p>`;
|
||
document.getElementById('emergencyModalActions').style.display = '';
|
||
document.getElementById('emergencyModal').classList.add('open');
|
||
}
|
||
|
||
function closeEmergencyModal() {
|
||
document.getElementById('emergencyModal').classList.remove('open');
|
||
}
|
||
|
||
async function confirmEmergency() {
|
||
document.getElementById('emergencyModalActions').style.display = 'none';
|
||
try {
|
||
const res = await fetch('/keyholder/cardlock/' + lockId + '/emergency-unlock', { method: 'POST' });
|
||
if (res.ok || res.status === 204) {
|
||
const hasKH2 = _currentLock && _currentLock.keyholderName;
|
||
const successText = hasKH2
|
||
? `✅ Notfall-Anfrage wurde gesendet. ${_currentLock.keyholderName} wurde benachrichtigt.`
|
||
: `✅ Notfall-Freigabe wurde ausgelöst. Das Lock öffnet sich jetzt.`;
|
||
document.getElementById('emergencyModalContent').innerHTML = `
|
||
<p style="font-size:0.88rem;color:#2ecc71;line-height:1.5;margin:0;">
|
||
${successText}
|
||
</p>`;
|
||
setTimeout(() => { closeEmergencyModal(); loadLock(); }, 2000);
|
||
}
|
||
} catch(e) {
|
||
document.getElementById('emergencyModalContent').innerHTML = `<p style="color:#e74c3c;">Fehler beim Senden der Anfrage.</p>`;
|
||
}
|
||
}
|
||
|
||
let tickInterval = null;
|
||
let hygienePanelTick = null;
|
||
|
||
function fmtCountdown(diffMs) {
|
||
const totalSec = Math.floor(diffMs / 1000);
|
||
const h = Math.floor(totalSec / 3600);
|
||
const m = Math.floor((totalSec % 3600) / 60);
|
||
const s = totalSec % 60;
|
||
return h > 0
|
||
? `${h}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`
|
||
: `${m}:${String(s).padStart(2,'0')}`;
|
||
}
|
||
|
||
// ── Keyholder-Aufgaben ──────────────────────────────────────────────────────
|
||
|
||
let activeAssignedTaskId = null;
|
||
|
||
function fmtDateTime(iso) {
|
||
const dt = new Date(iso);
|
||
return dt.toLocaleDateString('de-DE') + ', ' + dt.toLocaleTimeString('de-DE', { hour:'2-digit', minute:'2-digit' }) + ' Uhr';
|
||
}
|
||
|
||
const assignedTaskCache = {};
|
||
|
||
function renderAssignedTasks(lock) {
|
||
const area = document.getElementById('assignedTasksArea');
|
||
const tasks = lock.assignedTasks || [];
|
||
// Aufgaben ausblenden wenn bereits eine Aufgabe aktiv ist (nicht bei Keyholder-Freeze)
|
||
if (tasks.length === 0 || (lock.currentTask && lock.currentTask.trim())) {
|
||
area.style.display = 'none'; area.innerHTML = ''; return;
|
||
}
|
||
area.style.display = '';
|
||
area.innerHTML = tasks.map(t => {
|
||
assignedTaskCache[t.taskId] = t;
|
||
const deadline = new Date(t.acceptDeadline);
|
||
const remaining = deadline - Date.now();
|
||
const urgent = remaining < 60 * 60 * 1000; // < 1h
|
||
const titleEsc = (t.taskTitle || '').replace(/</g,'<').replace(/>/g,'>');
|
||
const descEsc = (t.taskDescription || '').replace(/</g,'<').replace(/>/g,'>');
|
||
const mins = t.taskMinutes > 0 ? ` · ${t.taskMinutes} Min.` : '';
|
||
return `<div onclick="openAssignedTaskModal('${t.taskId}')"
|
||
style="background:var(--color-card);border:1px solid ${urgent ? 'rgba(231,76,60,0.5)' : 'var(--color-secondary)'};
|
||
border-radius:10px;padding:0.75rem 1rem;margin-bottom:0.5rem;cursor:pointer;display:flex;align-items:flex-start;gap:0.6rem;"
|
||
onmouseover="this.style.borderColor='var(--color-primary)'" onmouseout="this.style.borderColor='${urgent ? 'rgba(231,76,60,0.5)' : 'var(--color-secondary)'}'">
|
||
<span style="font-size:1.3rem;flex-shrink:0;line-height:1.4;">✅</span>
|
||
<div style="flex:1;min-width:0;">
|
||
<div style="font-weight:700;font-size:0.92rem;margin-bottom:0.15rem;">${titleEsc}${mins}</div>
|
||
${descEsc ? `<div style="font-size:0.82rem;color:var(--color-muted);margin-bottom:0.25rem;line-height:1.4;">${descEsc}</div>` : ''}
|
||
<div style="font-size:0.78rem;color:var(--color-muted);">Gestellt: ${fmtDateTime(t.assignedAt)}</div>
|
||
<div style="font-size:0.78rem;color:${urgent ? '#e74c3c' : 'var(--color-muted)'};">Fällig bis: ${fmtDateTime(t.acceptDeadline)}</div>
|
||
</div>
|
||
<span style="font-size:0.75rem;color:var(--color-primary);font-weight:600;flex-shrink:0;align-self:center;">Details →</span>
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
function openAssignedTaskModal(taskId) {
|
||
const t = assignedTaskCache[taskId];
|
||
if (!t) return;
|
||
activeAssignedTaskId = taskId;
|
||
document.getElementById('assignedTaskModalError').style.display = 'none';
|
||
const titleEsc = (t.taskTitle || '').replace(/</g,'<').replace(/>/g,'>');
|
||
const descEsc = (t.taskDescription || '').replace(/</g,'<').replace(/>/g,'>');
|
||
const taskMinutes = t.taskMinutes || 0;
|
||
const penaltyFreezeMinutes = t.penaltyFreezeMinutes;
|
||
const penaltyRedCards = t.penaltyRedCards;
|
||
const minsHtml = taskMinutes > 0 ? `<div>Zeit: <strong>${taskMinutes} Minute${taskMinutes !== 1 ? 'n' : ''}</strong></div>` : '';
|
||
const descHtml = descEsc ? `<div style="margin-top:0.35rem;">${descEsc}</div>` : '';
|
||
document.getElementById('assignedTaskModalInfo').innerHTML =
|
||
`<div style="font-weight:700;font-size:0.97rem;color:var(--color-text);margin-bottom:0.4rem;">${titleEsc}</div>` +
|
||
descHtml + minsHtml +
|
||
`<div style="margin-top:0.5rem;">Gestellt am: <strong>${fmtDateTime(t.assignedAt)}</strong></div>` +
|
||
`<div>Annehmen bis: <strong>${fmtDateTime(t.acceptDeadline)}</strong></div>`;
|
||
const penaltyParts = [];
|
||
if (penaltyFreezeMinutes > 0) {
|
||
const d = Math.floor(penaltyFreezeMinutes / 1440);
|
||
const h = Math.floor((penaltyFreezeMinutes % 1440) / 60);
|
||
const m = penaltyFreezeMinutes % 60;
|
||
const parts = [];
|
||
if (d) parts.push(d + 'd');
|
||
if (h) parts.push(h + 'h');
|
||
if (m) parts.push(m + 'min');
|
||
penaltyParts.push('❄️ Einfrieren für ' + parts.join(' '));
|
||
}
|
||
if (penaltyRedCards > 0) penaltyParts.push('🔴 ' + penaltyRedCards + ' rote Karte' + (penaltyRedCards !== 1 ? 'n' : '') + ' hinzufügen');
|
||
document.getElementById('assignedTaskPenaltyInfo').innerHTML =
|
||
'<strong>Strafe bei Ablehnung:</strong><br>' + (penaltyParts.join('<br>') || '–');
|
||
document.getElementById('assignedTaskModal').style.display = 'flex';
|
||
}
|
||
|
||
function closeAssignedTaskModal() {
|
||
document.getElementById('assignedTaskModal').style.display = 'none';
|
||
activeAssignedTaskId = null;
|
||
}
|
||
|
||
async function respondAssignedTask(action) {
|
||
if (!activeAssignedTaskId) return;
|
||
const errEl = document.getElementById('assignedTaskModalError');
|
||
errEl.style.display = 'none';
|
||
try {
|
||
const res = await fetch(`/keyholder/cardlock/${lockId}/assigned-tasks/${activeAssignedTaskId}/${action}`, { method: 'POST' });
|
||
if (res.ok || res.status === 204) {
|
||
closeAssignedTaskModal();
|
||
loadLock();
|
||
} else {
|
||
const data = await res.json().catch(() => ({}));
|
||
errEl.textContent = data.error || 'Fehler.';
|
||
errEl.style.display = '';
|
||
}
|
||
} catch(e) {
|
||
errEl.textContent = 'Fehler bei der Verbindung.';
|
||
errEl.style.display = '';
|
||
}
|
||
}
|
||
|
||
document.getElementById('assignedTaskModal').addEventListener('click', e => {
|
||
if (e.target === e.currentTarget) closeAssignedTaskModal();
|
||
});
|
||
document.addEventListener('keydown', e => {
|
||
if (e.key === 'Escape' && document.getElementById('assignedTaskModal').style.display === 'flex')
|
||
closeAssignedTaskModal();
|
||
});
|
||
|
||
// ── Keyholder-Bar ──────────────────────────────────────────────────────────
|
||
|
||
function renderKeyholderBar(lock) {
|
||
const bar = document.getElementById('keyholderInfoBar');
|
||
const avatar = document.getElementById('keyholderInfoAvatar');
|
||
const name = document.getElementById('keyholderInfoName');
|
||
if (!lock.hasKeyholder || !lock.keyholderName) { bar.style.display = 'none'; return; }
|
||
bar.style.display = 'flex';
|
||
name.textContent = lock.keyholderName;
|
||
name.href = '/community/benutzer.html?userId=' + lock.keyholderUserId;
|
||
if (lock.keyholderProfilePic) {
|
||
avatar.innerHTML = `<img src="data:image/jpeg;base64,${lock.keyholderProfilePic}" alt="" style="width:100%;height:100%;object-fit:cover;">`;
|
||
} else {
|
||
avatar.textContent = '👤';
|
||
}
|
||
}
|
||
|
||
function renderNextCardPanel(lock) {
|
||
if (tickInterval) { clearInterval(tickInterval); tickInterval = null; }
|
||
|
||
const panel = document.getElementById('nextcardPanel');
|
||
const cardsDiv = document.getElementById('nextcardCards');
|
||
const overlay = document.getElementById('nextcardOverlay');
|
||
panel.style.display = '';
|
||
panel.classList.remove('drawable');
|
||
|
||
// Karten-Bilder rendern (Overlay bleibt erhalten)
|
||
cardsDiv.querySelectorAll('.nextcard-card-img').forEach(el => el.remove());
|
||
const total = lock.totalCards || 0;
|
||
const show = Math.min(total, 36);
|
||
for (let i = 0; i < show; i++) {
|
||
const img = document.createElement('img');
|
||
img.className = 'nextcard-card-img';
|
||
img.src = '/img/card.png';
|
||
img.alt = 'Karte';
|
||
cardsDiv.insertBefore(img, overlay);
|
||
}
|
||
|
||
// Overlay-Elemente
|
||
const timerBox = document.getElementById('nextcardTimerBox');
|
||
const taskBox = document.getElementById('nextcardTaskBox');
|
||
const countdown = document.getElementById('nextcardCountdown');
|
||
const frozenLabel = document.getElementById('nextcardFrozenLabel');
|
||
const overlayLabel = document.getElementById('nextcardOverlayLabel');
|
||
const taskText = document.getElementById('nextcardTaskText');
|
||
const taskDesc = document.getElementById('nextcardTaskDescription');
|
||
const taskCountdown = document.getElementById('nextcardTaskCountdown');
|
||
const btnErledigt = document.getElementById('btnTaskErledigt');
|
||
|
||
// Overlay-Reset
|
||
overlay.classList.remove('frozen', 'task');
|
||
timerBox.style.display = 'none';
|
||
taskBox.style.display = 'none';
|
||
countdown.style.display = 'none';
|
||
frozenLabel.style.display = 'none';
|
||
|
||
const frozenUntill = lock.frozenUntill ? new Date(lock.frozenUntill) : null;
|
||
const taskFrozenUntil = lock.taskFrozenUntil ? new Date(lock.taskFrozenUntil) : null;
|
||
const currentTask = lock.currentTask || null;
|
||
const currentTaskDesc = lock.currentTaskDescription || null;
|
||
|
||
// ── Zustand 1: Aktive Aufgabe (mit oder ohne Task-Timer) ─────────────
|
||
if (currentTask) {
|
||
overlay.style.display = '';
|
||
overlay.classList.add('task');
|
||
taskBox.style.display = '';
|
||
taskText.textContent = currentTask;
|
||
if (currentTaskDesc) {
|
||
taskDesc.textContent = currentTaskDesc;
|
||
taskDesc.style.display = '';
|
||
} else {
|
||
taskDesc.style.display = 'none';
|
||
}
|
||
if (taskFrozenUntil && taskFrozenUntil > new Date()) {
|
||
btnErledigt.disabled = true;
|
||
taskCountdown.style.display = '';
|
||
function tickTask() {
|
||
const diff = taskFrozenUntil - Date.now();
|
||
if (diff <= 0) {
|
||
taskCountdown.style.display = 'none';
|
||
btnErledigt.disabled = false;
|
||
clearInterval(tickInterval); tickInterval = null;
|
||
return;
|
||
}
|
||
taskCountdown.textContent = '⏱ noch ' + fmtCountdown(diff);
|
||
}
|
||
tickTask();
|
||
tickInterval = setInterval(tickTask, 1000);
|
||
} else {
|
||
btnErledigt.disabled = false;
|
||
taskCountdown.style.display = 'none';
|
||
}
|
||
return;
|
||
}
|
||
|
||
// ── Zustand 2: Nur Keyholder-Freeze (keine Aufgabe) ──────────────────
|
||
if (frozenUntill && frozenUntill > new Date()) {
|
||
overlay.style.display = '';
|
||
overlay.classList.add('frozen');
|
||
timerBox.style.display = '';
|
||
overlayLabel.style.display = 'none';
|
||
frozenLabel.style.display = '';
|
||
return;
|
||
}
|
||
|
||
// ── Normalzustand: ziehbar oder Countdown ───────────────────────────
|
||
overlayLabel.style.display = '';
|
||
overlayLabel.textContent = 'Nächste Karte ziehbar in';
|
||
countdown.style.display = '';
|
||
|
||
if (lock.openPicks > 0) {
|
||
overlay.style.display = 'none';
|
||
if (!lock.keyholderRequestedUnlock) {
|
||
panel.classList.add('drawable');
|
||
enableCardClick();
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (!lock.nextCardIn) { overlay.style.display = 'none'; return; }
|
||
|
||
overlay.style.display = '';
|
||
timerBox.style.display = '';
|
||
const target = new Date(lock.nextCardIn);
|
||
|
||
function tick() {
|
||
const diff = target - Date.now();
|
||
if (diff <= 0) {
|
||
overlay.style.display = 'none';
|
||
if (!lock.keyholderRequestedUnlock) {
|
||
panel.classList.add('drawable');
|
||
enableCardClick();
|
||
}
|
||
clearInterval(tickInterval); tickInterval = null;
|
||
return;
|
||
}
|
||
countdown.textContent = fmtCountdown(diff);
|
||
}
|
||
tick();
|
||
tickInterval = setInterval(tick, 1000);
|
||
}
|
||
|
||
function renderHygienePanel(lock) {
|
||
if (hygienePanelTick) { clearInterval(hygienePanelTick); hygienePanelTick = null; }
|
||
if (!lock.hygieneEnabled) return;
|
||
const panel = document.getElementById('hygienePanel');
|
||
const countdown = document.getElementById('hygieneCountdown');
|
||
const btn = document.getElementById('hygieneBtn');
|
||
panel.style.display = '';
|
||
btn.style.display = '';
|
||
|
||
if (lock.hygieneOpeningDue) {
|
||
countdown.textContent = 'Verfügbar';
|
||
countdown.style.display = '';
|
||
btn.disabled = false;
|
||
} else {
|
||
btn.disabled = true;
|
||
countdown.style.display = '';
|
||
let totalSeconds = lock.hygieneSecondsRemaining;
|
||
function tick() {
|
||
if (totalSeconds <= 0) {
|
||
countdown.style.display = 'none';
|
||
btn.disabled = false;
|
||
clearInterval(hygienePanelTick); hygienePanelTick = null;
|
||
return;
|
||
}
|
||
countdown.textContent = fmtCountdown(totalSeconds * 1000);
|
||
totalSeconds--;
|
||
}
|
||
tick();
|
||
hygienePanelTick = setInterval(tick, 1000);
|
||
}
|
||
}
|
||
|
||
function renderVerificationPanel(lock) {
|
||
const panel = document.getElementById('verificationPanel');
|
||
const dueBody = document.getElementById('verificationDueBody');
|
||
const pendingBody = document.getElementById('verificationPendingBody');
|
||
const doneBody = document.getElementById('verificationDoneBody');
|
||
if (!lock.verificationRequired) {
|
||
panel.style.display = 'none';
|
||
return;
|
||
}
|
||
panel.style.display = '';
|
||
window._pendingVerificationId = lock.verificationPendingId || null;
|
||
window._pendingVerificationCode = lock.verificationPendingCode || null;
|
||
|
||
if (!lock.verificationDue) {
|
||
// Abgeschlossen
|
||
dueBody.style.display = 'none';
|
||
pendingBody.style.display = 'none';
|
||
doneBody.style.display = '';
|
||
const up = lock.verificationUpvotes || 0;
|
||
const down = lock.verificationDownvotes || 0;
|
||
document.getElementById('verificationVotes').innerHTML =
|
||
`<span class="vote-up">👍 ${up}</span><span class="vote-down">👎 ${down}</span>`;
|
||
document.getElementById('btnVerificationRenew').style.display = down > up ? '' : 'none';
|
||
} else if (lock.verificationPendingId) {
|
||
// Gestartet, Bild ausstehend
|
||
dueBody.style.display = 'none';
|
||
pendingBody.style.display = '';
|
||
doneBody.style.display = 'none';
|
||
} else {
|
||
// Noch nicht gestartet
|
||
dueBody.style.display = '';
|
||
pendingBody.style.display = 'none';
|
||
doneBody.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
async function renewVerification() {
|
||
const btn = document.getElementById('btnVerificationRenew');
|
||
btn.disabled = true;
|
||
const res = await fetch('/keyholder/cardlock/' + lockId + '/verification/today', { method: 'DELETE' });
|
||
if (res.ok || res.status === 204) {
|
||
loadLock();
|
||
} else {
|
||
btn.disabled = false;
|
||
}
|
||
}
|
||
|
||
function renderCardsPanel(lock) {
|
||
const panel = document.getElementById('cardsPanel');
|
||
const display = document.getElementById('cardsDisplay');
|
||
|
||
if (!lock.showRemainingCards) {
|
||
panel.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
panel.style.display = '';
|
||
display.innerHTML = cardTypeGridHtml(lock.availableCardCounts || {});
|
||
}
|
||
|
||
// ── Aufgabe erledigen ──
|
||
async function taskErledigt() {
|
||
const btn = document.getElementById('btnTaskErledigt');
|
||
btn.disabled = true;
|
||
await fetch('/keyholder/cardlock/' + lockId + '/task/complete', { method: 'POST' });
|
||
loadLock();
|
||
}
|
||
|
||
// ── Karte ziehen ──
|
||
let drawnUnlockCode = null;
|
||
|
||
function enableCardClick() {
|
||
document.querySelectorAll('.nextcard-card-img').forEach(img => {
|
||
img.addEventListener('click', onCardClick, { once: true });
|
||
});
|
||
}
|
||
|
||
async function onCardClick() {
|
||
// Alle weiteren Klicks blockieren
|
||
document.querySelectorAll('.nextcard-card-img').forEach(img => {
|
||
img.removeEventListener('click', onCardClick);
|
||
img.style.pointerEvents = 'none';
|
||
});
|
||
openDrawModal();
|
||
}
|
||
|
||
function openDrawModal() {
|
||
const modal = document.getElementById('drawModal');
|
||
const inner = document.getElementById('flipCardInner');
|
||
const info = document.getElementById('drawCardInfo');
|
||
const green = document.getElementById('drawGreenChoice');
|
||
const actions = document.getElementById('drawModalActions');
|
||
|
||
// Reset
|
||
inner.classList.remove('flipped');
|
||
info.classList.remove('visible');
|
||
green.classList.remove('visible');
|
||
document.getElementById('drawGreenText').style.display = '';
|
||
document.getElementById('drawUnlockCode').style.display = 'none';
|
||
document.getElementById('drawTaskPendingHint').style.display = 'none';
|
||
document.getElementById('btnDrawOk').style.display = '';
|
||
document.getElementById('btnDrawUnlock').style.display = 'none';
|
||
document.getElementById('btnDrawKeep').style.display = 'none';
|
||
actions.style.display = 'none';
|
||
drawnUnlockCode = null;
|
||
modal.classList.add('open');
|
||
|
||
// Karte serverseitig ziehen
|
||
fetch('/keyholder/cardlock/' + lockId + '/draw', { method: 'POST' })
|
||
.then(r => r.ok ? r.json() : Promise.reject(r.status))
|
||
.then(dto => {
|
||
const def = CARD_LABELS[dto.card] || { name: dto.card, img: '/img/card.png', desc: '' };
|
||
drawnUnlockCode = dto.unlockCode || null;
|
||
|
||
// Nach 1 Sekunde umdrehen
|
||
setTimeout(() => {
|
||
document.getElementById('drawnCardImg').src = def.img;
|
||
document.getElementById('drawnCardImg').alt = def.name;
|
||
inner.classList.add('flipped');
|
||
|
||
// Nach Ende der Animation Info + Buttons einblenden
|
||
setTimeout(() => {
|
||
document.getElementById('drawCardName').textContent = def.name;
|
||
document.getElementById('drawCardDesc').textContent = def.desc;
|
||
info.classList.add('visible');
|
||
actions.style.display = '';
|
||
|
||
if (dto.taskPending) {
|
||
const msgs = {
|
||
'KEYHOLDER': '🔑 Dein Keyholder wählt für dich eine Aufgabe aus. Du wirst benachrichtigt, sobald eine Aufgabe zugewiesen wurde.',
|
||
'COMMUNITY': '🗳️ Die Community stimmt ab, welche Aufgabe du erhältst. Das Ergebnis steht in einer Stunde fest.',
|
||
'RANDOM': '🎲 Da es sich um ein Test-Lock handelt, wird nach einer Stunde automatisch eine zufällige Aufgabe ausgewählt.',
|
||
};
|
||
document.getElementById('drawTaskPendingText').textContent = msgs[dto.taskPending] || '';
|
||
document.getElementById('drawTaskPendingHint').style.display = '';
|
||
}
|
||
|
||
if (dto.card === 'GREEN') {
|
||
green.classList.add('visible');
|
||
document.getElementById('btnDrawOk').style.display = 'none';
|
||
document.getElementById('btnDrawUnlock').style.display = '';
|
||
document.getElementById('btnDrawKeep').style.display = '';
|
||
}
|
||
}, 700);
|
||
}, 1000);
|
||
})
|
||
.catch(() => {
|
||
closeDrawModal();
|
||
alert('Fehler beim Ziehen der Karte.');
|
||
});
|
||
}
|
||
|
||
function closeDrawModal() {
|
||
document.getElementById('drawModal').classList.remove('open');
|
||
loadLock();
|
||
}
|
||
|
||
function confirmUnlock() {
|
||
if (drawnUnlockCode) {
|
||
document.getElementById('drawUnlockCode').textContent = drawnUnlockCode;
|
||
document.getElementById('drawUnlockCode').style.display = 'block';
|
||
}
|
||
// Session beenden nach Bestätigung → Warn-Modal überspringen, direkt löschen
|
||
document.getElementById('btnDrawUnlock').style.display = 'none';
|
||
document.getElementById('btnDrawKeep').style.display = 'none';
|
||
document.getElementById('btnDrawOk').style.display = '';
|
||
document.getElementById('btnDrawOk').textContent = 'Session beenden';
|
||
document.getElementById('btnDrawOk').onclick = () => lockLoeschen();
|
||
}
|
||
|
||
async function keepGreenCard() {
|
||
await fetch('/keyholder/cardlock/' + lockId + '/green/keep', { method: 'POST' });
|
||
closeDrawModal();
|
||
}
|
||
|
||
// ── Hygiene-Öffnung ──
|
||
let hygieneTickInterval = null;
|
||
|
||
async function openHygieneModal() {
|
||
const res = await fetch('/keyholder/cardlock/' + lockId + '/hygiene/start', { method: 'POST' });
|
||
if (!res.ok) { alert('Fehler beim Starten der Hygiene-Öffnung.'); return; }
|
||
const data = await res.json();
|
||
|
||
document.getElementById('hygieneCurrentCode').textContent = data.unlockCode;
|
||
document.getElementById('hygienePhase1').style.display = '';
|
||
document.getElementById('hygienePhase2').style.display = 'none';
|
||
document.getElementById('hygieneModal').classList.add('open');
|
||
|
||
const durationSec = (data.durationMinutes || 30) * 60;
|
||
const deadline = Date.now() + durationSec * 1000;
|
||
const timerEl = document.getElementById('hygieneTimer');
|
||
const labelEl = document.getElementById('hygieneTimerLabel');
|
||
|
||
if (hygieneTickInterval) clearInterval(hygieneTickInterval);
|
||
hygieneTickInterval = setInterval(() => {
|
||
const diff = deadline - Date.now();
|
||
if (diff > 0) {
|
||
timerEl.classList.remove('overtime');
|
||
labelEl.textContent = 'Verbleibende Zeit';
|
||
timerEl.textContent = fmtCountdown(diff);
|
||
} else {
|
||
timerEl.classList.add('overtime');
|
||
labelEl.textContent = 'Überschreitung';
|
||
timerEl.textContent = '+' + fmtCountdown(-diff);
|
||
}
|
||
}, 1000);
|
||
}
|
||
|
||
async function endHygieneOpening() {
|
||
if (hygieneTickInterval) { clearInterval(hygieneTickInterval); hygieneTickInterval = null; }
|
||
|
||
const isTtlock = _currentLock && _currentLock.controllType === 'TTLOCK';
|
||
if (isTtlock) {
|
||
document.getElementById('hygieneModal').classList.remove('open');
|
||
document.getElementById('ttlLoadingModal').classList.add('open');
|
||
}
|
||
|
||
const res = await fetch('/keyholder/cardlock/' + lockId + '/hygiene/end', { method: 'POST' });
|
||
|
||
if (isTtlock) {
|
||
document.getElementById('ttlLoadingModal').classList.remove('open');
|
||
if (!res.ok) { alert('Fehler beim Beenden der Hygiene-Öffnung.'); return; }
|
||
loadLock();
|
||
return;
|
||
}
|
||
|
||
if (!res.ok) { alert('Fehler beim Beenden der Hygiene-Öffnung.'); return; }
|
||
const data = await res.json();
|
||
|
||
document.getElementById('hygienePhase1').style.display = 'none';
|
||
const phase2 = document.getElementById('hygienePhase2');
|
||
phase2.style.display = 'flex';
|
||
document.getElementById('hygieneNewCode').textContent = data.newUnlockCode;
|
||
}
|
||
|
||
function closeHygieneModal() {
|
||
document.getElementById('hygieneModal').classList.remove('open');
|
||
if (hygieneTickInterval) { clearInterval(hygieneTickInterval); hygieneTickInterval = null; }
|
||
if (hygieneScrambleTimer) { clearInterval(hygieneScrambleTimer); hygieneScrambleTimer = null; }
|
||
if (hygieneScrambleCd) { clearInterval(hygieneScrambleCd); hygieneScrambleCd = null; }
|
||
loadLock();
|
||
}
|
||
|
||
let hygieneScrambleTimer = null;
|
||
let hygieneScrambleCd = null;
|
||
|
||
function startHygieneScramble() {
|
||
const codeEl = document.getElementById('hygieneNewCode');
|
||
const hintEl = document.getElementById('hygienePhase2Hint');
|
||
const cdEl = document.getElementById('hygieneScrambleCountdown');
|
||
const btnEl = document.getElementById('hygienePhase2Btn');
|
||
const realCode = codeEl.textContent;
|
||
const len = realCode.length;
|
||
const DURATION = 3 * 60;
|
||
let remaining = DURATION;
|
||
let stopped = false;
|
||
|
||
function randomCode() {
|
||
return Array.from({ length: len }, () => Math.floor(Math.random() * 10)).join('');
|
||
}
|
||
function finish() {
|
||
stopped = true;
|
||
clearInterval(hygieneScrambleTimer); hygieneScrambleTimer = null;
|
||
clearInterval(hygieneScrambleCd); hygieneScrambleCd = null;
|
||
closeHygieneModal();
|
||
}
|
||
|
||
hintEl.style.display = 'none';
|
||
cdEl.style.display = '';
|
||
document.querySelector('#hygieneModal h3').textContent = 'Nun vergessen wir den Code…';
|
||
btnEl.textContent = 'Abbrechen';
|
||
btnEl.onclick = finish;
|
||
|
||
function updateCd() {
|
||
const m = Math.floor(remaining / 60);
|
||
const s = remaining % 60;
|
||
cdEl.textContent = `${m}:${String(s).padStart(2,'0')}`;
|
||
}
|
||
updateCd();
|
||
|
||
hygieneScrambleTimer = setInterval(() => { if (!stopped) codeEl.textContent = randomCode(); }, 1000);
|
||
hygieneScrambleCd = setInterval(() => {
|
||
if (stopped) return;
|
||
remaining--;
|
||
updateCd();
|
||
if (remaining <= 0) finish();
|
||
}, 1000);
|
||
}
|
||
|
||
// ── Temp-Öffnung (CARD / TASK / etc.) ──
|
||
|
||
function renderTempOpeningPanel(lock) {
|
||
if (lock.tempOpeningActive) {
|
||
document.getElementById('tempOpeningCode').textContent = lock.tempOpeningUnlockCode || '–';
|
||
document.getElementById('tempPhase1').style.display = '';
|
||
document.getElementById('tempPhase2').style.display = 'none';
|
||
document.querySelector('#tempOpeningModal h3').textContent = 'Lock geöffnet';
|
||
document.getElementById('tempOpeningModal').classList.add('open');
|
||
} else {
|
||
document.getElementById('tempOpeningModal').classList.remove('open');
|
||
}
|
||
}
|
||
|
||
async function endTempOpeningFlow() {
|
||
const isTtlock = _currentLock && _currentLock.controllType === 'TTLOCK';
|
||
const isTrust = _currentLock && _currentLock.controllType === 'TRUST';
|
||
|
||
if (isTtlock) {
|
||
document.getElementById('tempOpeningModal').classList.remove('open');
|
||
document.getElementById('ttlLoadingModal').classList.add('open');
|
||
}
|
||
|
||
const res = await fetch('/keyholder/cardlock/' + lockId + '/hygiene/end', { method: 'POST' });
|
||
|
||
if (isTtlock) {
|
||
document.getElementById('ttlLoadingModal').classList.remove('open');
|
||
if (!res.ok) { alert('Fehler beim Beenden der Öffnung.'); return; }
|
||
loadLock();
|
||
return;
|
||
}
|
||
|
||
if (!res.ok) { alert('Fehler beim Beenden der Öffnung.'); return; }
|
||
const data = await res.json();
|
||
|
||
if (isTrust || !data.newUnlockCode) {
|
||
document.getElementById('tempOpeningModal').classList.remove('open');
|
||
loadLock();
|
||
return;
|
||
}
|
||
|
||
// UNLOCK_CODE: neuen Code anzeigen
|
||
document.getElementById('tempPhase1').style.display = 'none';
|
||
document.getElementById('tempPhase2').style.display = 'flex';
|
||
document.getElementById('tempNewCode').textContent = data.newUnlockCode;
|
||
document.getElementById('tempPhase2Hint').style.display = '';
|
||
document.getElementById('tempPhase2Btn').textContent = 'OK';
|
||
document.getElementById('tempPhase2Btn').onclick = startTempScramble;
|
||
document.getElementById('tempScrambleCountdown').style.display = 'none';
|
||
document.querySelector('#tempOpeningModal h3').textContent = 'Lock geöffnet';
|
||
}
|
||
|
||
function closeTempOpeningModal() {
|
||
document.getElementById('tempOpeningModal').classList.remove('open');
|
||
if (tempScrambleTimer) { clearInterval(tempScrambleTimer); tempScrambleTimer = null; }
|
||
if (tempScrambleCd) { clearInterval(tempScrambleCd); tempScrambleCd = null; }
|
||
loadLock();
|
||
}
|
||
|
||
let tempScrambleTimer = null;
|
||
let tempScrambleCd = null;
|
||
|
||
function startTempScramble() {
|
||
const codeEl = document.getElementById('tempNewCode');
|
||
const hintEl = document.getElementById('tempPhase2Hint');
|
||
const cdEl = document.getElementById('tempScrambleCountdown');
|
||
const btnEl = document.getElementById('tempPhase2Btn');
|
||
const realCode = codeEl.textContent;
|
||
const len = realCode.length;
|
||
const DURATION = 3 * 60;
|
||
let remaining = DURATION;
|
||
let stopped = false;
|
||
|
||
function randomCode() {
|
||
return Array.from({ length: len }, () => Math.floor(Math.random() * 10)).join('');
|
||
}
|
||
function finish() {
|
||
stopped = true;
|
||
clearInterval(tempScrambleTimer); tempScrambleTimer = null;
|
||
clearInterval(tempScrambleCd); tempScrambleCd = null;
|
||
closeTempOpeningModal();
|
||
}
|
||
|
||
hintEl.style.display = 'none';
|
||
cdEl.style.display = '';
|
||
document.querySelector('#tempOpeningModal h3').textContent = 'Nun vergessen wir den Code…';
|
||
btnEl.textContent = 'Abbrechen';
|
||
btnEl.onclick = finish;
|
||
|
||
function updateCd() {
|
||
const m = Math.floor(remaining / 60);
|
||
const s = remaining % 60;
|
||
cdEl.textContent = `${m}:${String(s).padStart(2,'0')}`;
|
||
}
|
||
updateCd();
|
||
|
||
tempScrambleTimer = setInterval(() => { if (!stopped) codeEl.textContent = randomCode(); }, 1000);
|
||
tempScrambleCd = setInterval(() => {
|
||
if (stopped) return;
|
||
remaining--;
|
||
updateCd();
|
||
if (remaining <= 0) finish();
|
||
}, 1000);
|
||
}
|
||
|
||
// ── Lock beenden ──
|
||
function lockBeendenFragen() {
|
||
document.getElementById('warnModalUnlockCode').textContent = _currentLock ? (_currentLock.unlockCode || '–') : '–';
|
||
document.getElementById('warnModal').classList.add('open');
|
||
}
|
||
|
||
function closeWarnModal() {
|
||
document.getElementById('warnModal').classList.remove('open');
|
||
}
|
||
|
||
async function lockLoeschen() {
|
||
closeWarnModal();
|
||
if (_currentLock && _currentLock.controllType === 'TTLOCK') {
|
||
document.getElementById('ttlLoadingModal').classList.add('open');
|
||
}
|
||
try {
|
||
await fetch('/keyholder/cardlock/' + lockId, { method: 'DELETE' });
|
||
} catch (_) { /* ignorieren */ }
|
||
window.location.href = '/userhome.html';
|
||
}
|
||
|
||
document.addEventListener('keydown', e => {
|
||
if (e.key === 'Escape') closeWarnModal();
|
||
});
|
||
|
||
// ── Verifikation ──
|
||
let currentVerificationId = null;
|
||
|
||
async function startVerification() {
|
||
let verificationId, code;
|
||
if (window._pendingVerificationId) {
|
||
verificationId = window._pendingVerificationId;
|
||
code = window._pendingVerificationCode;
|
||
} else {
|
||
const res = await fetch('/keyholder/cardlock/' + lockId + '/verification/start', { method: 'POST' });
|
||
if (!res.ok) return;
|
||
const data = await res.json();
|
||
verificationId = data.verificationId;
|
||
code = data.code;
|
||
}
|
||
currentVerificationId = verificationId;
|
||
document.getElementById('verificationCode').textContent = code;
|
||
document.getElementById('verificationImageInput').value = '';
|
||
document.getElementById('verificationUploadText').textContent = '📁 Bild auswählen / aufnehmen';
|
||
document.getElementById('verificationPreview').style.display = 'none';
|
||
document.getElementById('verificationDoneBtn').disabled = true;
|
||
document.getElementById('verificationModal').style.display = 'flex';
|
||
}
|
||
|
||
let _verificationBlob = null;
|
||
|
||
function onVerificationImageSelected(input) {
|
||
if (!input.files || !input.files[0]) return;
|
||
const file = input.files[0];
|
||
document.getElementById('verificationUploadText').textContent = '✓ ' + file.name;
|
||
document.getElementById('verificationDoneBtn').disabled = true;
|
||
_verificationBlob = null;
|
||
|
||
const img = new Image();
|
||
const reader = new FileReader();
|
||
reader.onload = e => {
|
||
img.onload = () => {
|
||
const MAX = 1024;
|
||
let w = img.width, h = img.height;
|
||
if (w > MAX || h > MAX) {
|
||
const scale = MAX / Math.max(w, h);
|
||
w = Math.round(w * scale);
|
||
h = Math.round(h * scale);
|
||
}
|
||
const canvas = document.createElement('canvas');
|
||
canvas.width = w; canvas.height = h;
|
||
canvas.getContext('2d').drawImage(img, 0, 0, w, h);
|
||
canvas.toBlob(blob => {
|
||
_verificationBlob = blob;
|
||
document.getElementById('verificationPreviewImg').src = canvas.toDataURL('image/jpeg');
|
||
document.getElementById('verificationPreview').style.display = '';
|
||
document.getElementById('verificationDoneBtn').disabled = false;
|
||
}, 'image/jpeg', 0.9);
|
||
};
|
||
img.src = e.target.result;
|
||
};
|
||
reader.readAsDataURL(file);
|
||
}
|
||
|
||
async function completeVerification() {
|
||
const btn = document.getElementById('verificationDoneBtn');
|
||
btn.disabled = true;
|
||
const form = new FormData();
|
||
form.append('image', _verificationBlob || document.getElementById('verificationImageInput').files[0], 'verification.jpg');
|
||
const res = await fetch('/keyholder/cardlock/' + lockId + '/verification/' + currentVerificationId + '/complete', {
|
||
method: 'POST',
|
||
body: form
|
||
});
|
||
if (res.ok) {
|
||
document.getElementById('verificationModal').style.display = 'none';
|
||
loadLock();
|
||
} else {
|
||
btn.disabled = false;
|
||
}
|
||
}
|
||
|
||
function closeVerificationModal() {
|
||
document.getElementById('verificationModal').style.display = 'none';
|
||
}
|
||
|
||
// ── Auto-Refresh alle 60 Sekunden ──
|
||
setInterval(loadLock, 60_000);
|
||
</script>
|
||
</body>
|
||
</html>
|