Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
1733 lines
97 KiB
HTML
1733 lines
97 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>Keyholder – xXx Sphere</title>
|
||
<link rel="stylesheet" href="/css/variables.css">
|
||
<link rel="stylesheet" href="/css/style.css">
|
||
<link rel="stylesheet" href="/css/time-picker.css">
|
||
<style>
|
||
.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) */
|
||
/* ── 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="tpChange('at',-1,'d')">−</button><input type="text" id="at_d" value="0" readonly><button type="button" onclick="tpChange('at',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
|
||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('at',-1,'h')">−</button><input type="text" id="at_h" value="01" readonly><button type="button" onclick="tpChange('at',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
|
||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('at',-1,'m')">−</button><input type="text" id="at_m" value="00" readonly><button type="button" onclick="tpChange('at',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="tpChange('atf',-1,'d')">−</button><input type="text" id="atf_d" value="0" readonly><button type="button" onclick="tpChange('atf',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
|
||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('atf',-1,'h')">−</button><input type="text" id="atf_h" value="04" readonly><button type="button" onclick="tpChange('atf',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
|
||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('atf',-1,'m')">−</button><input type="text" id="atf_m" value="00" readonly><button type="button" onclick="tpChange('atf',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="tpChange('freeze',-1,'d')">−</button><input type="text" id="freeze_d" value="0" readonly><button type="button" onclick="tpChange('freeze',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
|
||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('freeze',-1,'h')">−</button><input type="text" id="freeze_h" value="04" readonly><button type="button" onclick="tpChange('freeze',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
|
||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('freeze',-1,'m')">−</button><input type="text" id="freeze_m" value="00" readonly><button type="button" onclick="tpChange('freeze',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 src="/js/time-picker.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 <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 <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 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 = tpToMinutes('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 ? tpToMinutes('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;
|
||
|
||
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 = tpToMinutes('freeze');
|
||
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 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 ? (tpToMinutes('ctpf') || 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="tpChange('ctpf',-1,'d')">−</button><input type="text" id="ctpf_d" value="0" readonly><button type="button" onclick="tpChange('ctpf',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
|
||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('ctpf',-1,'h')">−</button><input type="text" id="ctpf_h" value="04" readonly><button type="button" onclick="tpChange('ctpf',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
|
||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('ctpf',-1,'m')">−</button><input type="text" id="ctpf_m" value="00" readonly><button type="button" onclick="tpChange('ctpf',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>
|