Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
1143 lines
61 KiB
HTML
1143 lines
61 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>Location – xXx Sphere</title>
|
||
<link rel="stylesheet" href="/css/variables.css">
|
||
<link rel="stylesheet" href="/css/style.css">
|
||
<style>
|
||
.back-link { display:inline-flex; align-items:center; gap:0.35rem; color:var(--color-muted); font-size:0.88rem; text-decoration:none; margin-bottom:1rem; }
|
||
.back-link:hover { color:var(--color-primary); }
|
||
|
||
.loc-header { display:flex; gap:1rem; align-items:flex-start; margin-bottom:1.25rem; flex-wrap:wrap; }
|
||
.loc-avatar { width:120px; height:120px; border-radius:12px; background:var(--color-secondary); object-fit:cover; flex-shrink:0; display:flex; align-items:center; justify-content:center; font-size:2.5rem; overflow:hidden; border:2px solid var(--color-secondary); }
|
||
.loc-avatar img { width:100%; height:100%; object-fit:cover; }
|
||
.loc-meta { flex:1; min-width:0; }
|
||
.loc-name { font-size:1.4rem; font-weight:700; margin:0 0 0.3rem; }
|
||
.loc-city { color:var(--color-muted); font-size:0.88rem; margin-bottom:0.4rem; }
|
||
.loc-desc { font-size:0.93rem; line-height:1.55; white-space:pre-wrap; word-break:break-word; margin-top:0.5rem; }
|
||
|
||
.section-title { font-size:1rem; font-weight:700; margin:1.5rem 0 0.75rem; display:flex; align-items:center; justify-content:space-between; }
|
||
|
||
.hours-table { width:100%; border-collapse:collapse; font-size:0.88rem; }
|
||
.hours-table td { padding:0.3rem 0.5rem; border-bottom:1px solid var(--color-secondary); }
|
||
.hours-table td:first-child { font-weight:500; width:100px; }
|
||
.hours-closed { color:var(--color-muted); }
|
||
|
||
.gallery-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(180px,1fr)); gap:0.6rem; }
|
||
.gallery-img-wrap { position:relative; aspect-ratio:1; border-radius:8px; overflow:hidden; background:var(--color-secondary); }
|
||
.gallery-img-wrap img { width:100%; height:100%; object-fit:cover; cursor:pointer; transition:opacity 0.15s; }
|
||
.gallery-img-wrap img:hover { opacity:0.88; }
|
||
.gallery-del-btn { position:absolute; top:4px; right:4px; background:rgba(0,0,0,.6); border:none; color:#fff; border-radius:50%; width:22px; height:22px; font-size:0.7rem; cursor:pointer; display:flex; align-items:center; justify-content:center; padding:0; margin:0; }
|
||
.gallery-upload-btn { aspect-ratio:1; border:2px dashed var(--color-secondary); border-radius:8px; display:flex; align-items:center; justify-content:center; font-size:1.5rem; color:var(--color-muted); cursor:pointer; transition:border-color 0.15s; background:none; }
|
||
.gallery-upload-btn:hover { border-color:var(--color-primary); color:var(--color-primary); }
|
||
|
||
.event-list { display:flex; flex-direction:column; gap:0.75rem; }
|
||
.event-card { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:10px; display:flex; gap:0.75rem; padding:0.75rem; text-decoration:none; color:inherit; transition:border-color 0.15s; cursor:pointer; }
|
||
.event-card:hover { border-color:var(--color-primary); }
|
||
.event-card-img { width:64px; height:64px; border-radius:8px; object-fit:cover; background:var(--color-secondary); flex-shrink:0; overflow:hidden; display:flex; align-items:center; justify-content:center; font-size:1.4rem; }
|
||
.event-card-img img { width:100%; height:100%; object-fit:cover; }
|
||
.event-card-body { flex:1; min-width:0; }
|
||
.event-card-title { font-weight:600; font-size:0.92rem; margin:0 0 0.2rem; }
|
||
.event-card-date { font-size:0.78rem; color:var(--color-muted); }
|
||
.event-card-attendees { font-size:0.78rem; color:var(--color-muted); }
|
||
|
||
/* Modal */
|
||
.modal-overlay { display:none; position:fixed; inset:0; background:rgba(0,0,0,.6); z-index:200; align-items:center; justify-content:center; }
|
||
.modal-overlay.open { display:flex; }
|
||
.modal { background:var(--color-card); border-radius:12px; width:min(520px,95vw); max-height:90vh; overflow-y:auto; padding:1.5rem; }
|
||
.modal h3 { margin:0 0 1rem; }
|
||
.modal-footer { display:flex; gap:0.75rem; justify-content:flex-end; margin-top:1.25rem; flex-wrap:wrap; }
|
||
.hours-grid { display:grid; grid-template-columns:auto 1fr 1fr auto; gap:0.4rem 0.5rem; align-items:center; font-size:0.85rem; margin-top:0.5rem; }
|
||
.img-preview { width:80px; height:80px; border-radius:8px; object-fit:cover; background:var(--color-secondary); display:flex; align-items:center; justify-content:center; font-size:1.5rem; flex-shrink:0; overflow:hidden; border:1px solid var(--color-secondary); }
|
||
.img-preview img { width:100%; height:100%; object-fit:cover; }
|
||
.img-row { display:flex; gap:0.75rem; align-items:center; margin-bottom:0.5rem; }
|
||
.suggest-list { position:absolute; top:100%; left:0; right:0; background:var(--color-card); border:1px solid var(--color-secondary); border-top:none; border-radius:0 0 6px 6px; z-index:50; display:none; list-style:none; margin:0; padding:0; max-height:220px; overflow-y:auto; }
|
||
|
||
/* Lightbox */
|
||
.lb { display:none; position:fixed; inset:0; background:rgba(0,0,0,.9); z-index:300; align-items:center; justify-content:center; }
|
||
.lb.open { display:flex; }
|
||
.lb img { max-width:85vw; max-height:90vh; border-radius:8px; object-fit:contain; }
|
||
.lb-close { position:absolute; top:1rem; right:1rem; background:none; border:none; color:#fff; font-size:1.5rem; cursor:pointer; }
|
||
.lb-nav { position:absolute; top:50%; transform:translateY(-50%); background:rgba(255,255,255,.15); border:none; color:#fff; font-size:2.5rem; line-height:1; cursor:pointer; padding:0.3rem 0.8rem; border-radius:8px; transition:background 0.15s; user-select:none; }
|
||
.lb-nav:hover { background:rgba(255,255,255,.3); }
|
||
.lb-nav:disabled { opacity:0.2; cursor:default; }
|
||
.lb-prev { left:1rem; }
|
||
.lb-next { right:1rem; }
|
||
|
||
.owner-badge { display:inline-flex; align-items:center; gap:0.3rem; font-size:0.75rem; background:var(--color-secondary); border-radius:4px; padding:0.2rem 0.5rem; color:var(--color-muted); margin-top:0.3rem; }
|
||
.owner-actions { display:flex; gap:0.5rem; flex-wrap:wrap; margin-top:0.75rem; }
|
||
|
||
/* Tabs */
|
||
.tab-bar { display:flex; gap:0; border-bottom:2px solid var(--color-secondary); margin-bottom:1.25rem; flex-wrap:wrap; }
|
||
.tab-btn { background:none; border:none; border-bottom:2px solid transparent; color:var(--color-muted); cursor:pointer; font-size:0.88rem; font-weight:600; padding:0.65rem 1rem; margin-bottom:-2px; border-radius:0; width:auto; margin-top:0; transition:color 0.15s, border-color 0.15s; white-space:nowrap; }
|
||
.tab-btn:hover { color:var(--color-text); background:none; }
|
||
.tab-btn.active { color:var(--color-text); border-bottom-color:var(--color-primary); }
|
||
.tab-panel { display:none; }
|
||
.tab-panel.active { display:block; }
|
||
|
||
/* Posteingang */
|
||
.inbox-list { display:flex; flex-direction:column; gap:0.5rem; }
|
||
.inbox-item { display:flex; align-items:center; gap:0.65rem; padding:0.65rem 0.75rem; border-radius:8px; background:var(--color-secondary); cursor:pointer; transition:opacity 0.15s; }
|
||
.inbox-item:hover { opacity:0.85; }
|
||
.inbox-avatar { width:36px; height:36px; border-radius:50%; background:var(--color-card); display:flex; align-items:center; justify-content:center; font-size:0.9rem; flex-shrink:0; overflow:hidden; }
|
||
.inbox-avatar img { width:100%; height:100%; object-fit:cover; }
|
||
.inbox-info { flex:1; min-width:0; }
|
||
.inbox-name { font-weight:600; font-size:0.88rem; }
|
||
.inbox-preview { font-size:0.78rem; color:var(--color-muted); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||
.inbox-unread { background:var(--color-primary); color:#fff; font-size:0.65rem; font-weight:700; border-radius:9999px; padding:0.1rem 0.35rem; flex-shrink:0; }
|
||
|
||
/* Posteingang Chat */
|
||
.inbox-chat { display:none; flex-direction:column; gap:0; margin-top:0.75rem; border:1px solid var(--color-secondary); border-radius:10px; overflow:hidden; }
|
||
.inbox-chat.open { display:flex; }
|
||
.inbox-chat-header { display:flex; align-items:center; gap:0.5rem; padding:0.65rem 0.9rem; background:var(--color-secondary); font-weight:600; font-size:0.88rem; }
|
||
.inbox-chat-back { background:none; border:none; color:var(--color-muted); cursor:pointer; font-size:1.1rem; padding:0; margin:0; width:auto; line-height:1; }
|
||
.inbox-chat-close { background:none; border:none; color:var(--color-muted); cursor:pointer; font-size:1rem; padding:0.15rem 0.35rem; margin:0 0 0 auto; width:auto; line-height:1; border-radius:4px; transition:background 0.15s, color 0.15s; }
|
||
.inbox-chat-close:hover { background:var(--color-card); color:var(--color-text); }
|
||
.inbox-chat-messages { max-height:320px; overflow-y:auto; padding:0.75rem 1rem; display:flex; flex-direction:column; gap:0.4rem; }
|
||
.inbox-bubble-wrap { display:flex; flex-direction:column; }
|
||
.inbox-bubble-wrap.me { align-items:flex-end; }
|
||
.inbox-bubble-wrap.them { align-items:flex-start; }
|
||
.inbox-bubble { max-width:75%; padding:0.45rem 0.8rem; border-radius:12px; font-size:0.88rem; line-height:1.4; word-break:break-word; }
|
||
.inbox-bubble-wrap.me .inbox-bubble { background:var(--color-primary); color:#fff; border-bottom-right-radius:4px; }
|
||
.inbox-bubble-wrap.them .inbox-bubble { background:var(--color-secondary); color:var(--color-text); border-bottom-left-radius:4px; }
|
||
.inbox-bubble-time { font-size:0.68rem; color:var(--color-muted); margin-top:0.1rem; padding:0 0.2rem; }
|
||
.inbox-reply-area { display:flex; gap:0.5rem; padding:0.6rem 0.9rem; border-top:1px solid var(--color-secondary); align-items:center; }
|
||
.inbox-reply-area input { flex:1; }
|
||
.inbox-reply-btn { width:auto; margin-top:0; padding:0.5rem 1rem; flex-shrink:0; }
|
||
.inbox-lock-hint { font-size:0.8rem; color:var(--color-muted); padding:0.5rem 0.9rem; border-top:1px solid var(--color-secondary); background:var(--color-secondary); }
|
||
.inbox-reply-trigger { padding:0.6rem 0.9rem; border-top:1px solid var(--color-secondary); }
|
||
.inbox-reply-trigger .btn { font-size:0.85rem; }
|
||
</style>
|
||
</head>
|
||
<body class="app">
|
||
|
||
<!-- ── Admin-Autocomplete (außerhalb .main, damit overflow-y:auto nicht clippt) ── -->
|
||
<ul id="adminSearchList" style="position:fixed;display:none;background:var(--color-card);border:1px solid var(--color-secondary);border-radius:6px;z-index:400;list-style:none;margin:0;padding:0;max-height:200px;overflow-y:auto;box-shadow:0 4px 12px rgba(0,0,0,.15);"></ul>
|
||
<ul id="ownerSearchList" style="position:fixed;display:none;background:var(--color-card);border:1px solid var(--color-secondary);border-radius:6px;z-index:400;list-style:none;margin:0;padding:0;max-height:200px;overflow-y:auto;box-shadow:0 4px 12px rgba(0,0,0,.15);"></ul>
|
||
|
||
<div class="main">
|
||
<div class="content">
|
||
<a href="/community/locations.html" class="back-link">← Locations</a>
|
||
|
||
<div id="content">
|
||
<p style="color:var(--color-muted);">Wird geladen…</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Edit-Modal ──────────────────────────────────────────────────────────── -->
|
||
<div class="modal-overlay" id="editModal">
|
||
<div class="modal">
|
||
<h3>Location bearbeiten</h3>
|
||
<div class="img-row">
|
||
<div class="img-preview" id="editPicPreview">📍</div>
|
||
<div>
|
||
<label class="btn" style="display:inline-block;cursor:pointer;font-size:0.85rem;">
|
||
Profilbild ändern
|
||
<input type="file" id="editPicFile" accept="image/*" style="display:none;" onchange="onEditPicChange(this)">
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<label>Name *</label>
|
||
<input type="text" id="editName" maxlength="200">
|
||
<label>Beschreibung <span style="color:var(--color-muted);font-size:0.8rem;">(max. 1000 Zeichen)</span></label>
|
||
<textarea id="editDesc" maxlength="1000" rows="4" style="resize:vertical;width:100%;box-sizing:border-box;padding:0.65rem 0.9rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:1rem;outline:none;font-family:inherit;transition:border-color 0.2s;" onfocus="this.style.borderColor='var(--color-primary)'" onblur="this.style.borderColor='var(--color-secondary)'"></textarea>
|
||
<label>Adresse</label>
|
||
<div id="editStadtRow">
|
||
<div style="position:relative;">
|
||
<input type="text" id="editCity" placeholder="Straße, Hausnummer, Stadt…" autocomplete="off"
|
||
style="width:100%;box-sizing:border-box;padding:0.55rem 2rem 0.55rem 0.8rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;"
|
||
oninput="onEditCityInput()">
|
||
<button id="editCityClear" onclick="clearEditCity()" title="Auswahl aufheben"
|
||
style="display:none;position:absolute;right:0.4rem;top:50%;transform:translateY(-50%);background:none;border:none;color:var(--color-muted);font-size:1.1rem;cursor:pointer;padding:0.1rem 0.3rem;margin:0;width:auto;line-height:1;">×</button>
|
||
<ul id="editCitySuggestions" style="display:none;position:absolute;top:100%;left:0;right:0;
|
||
background:var(--color-card);border:1px solid var(--color-secondary);border-radius:6px;
|
||
z-index:100;list-style:none;margin:0.2rem 0 0;padding:0;max-height:200px;overflow-y:auto;"></ul>
|
||
</div>
|
||
<div id="editLocMsg" style="font-size:0.82rem;color:var(--color-muted);margin-top:0.25rem;min-height:1.1em;"></div>
|
||
</div>
|
||
<label style="margin-top:0.75rem;display:block;">Öffnungszeiten</label>
|
||
<div class="hours-grid" id="editHoursGrid"></div>
|
||
<div class="modal-footer">
|
||
<button class="btn" style="background:var(--color-secondary);color:var(--color-text);" onclick="closeEditModal()">Abbrechen</button>
|
||
<button class="btn" id="editSubmitBtn" onclick="submitEdit()">Speichern</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Event erstellen/bearbeiten Modal ───────────────────────────────────── -->
|
||
<div class="modal-overlay" id="eventModal">
|
||
<div class="modal">
|
||
<h3 id="eventModalTitle">Veranstaltung erstellen</h3>
|
||
<div class="img-row">
|
||
<div class="img-preview" id="eventPicPreview">🗓</div>
|
||
<div>
|
||
<label class="btn" style="display:inline-block;cursor:pointer;font-size:0.85rem;">
|
||
Bild wählen
|
||
<input type="file" id="eventPicFile" accept="image/*" style="display:none;" onchange="onEventPicChange(this)">
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<label>Titel *</label>
|
||
<input type="text" id="eventTitle" maxlength="200">
|
||
<label>Beschreibung <span style="color:var(--color-muted);font-size:0.8rem;">(max. 1000 Zeichen)</span></label>
|
||
<textarea id="eventDesc" maxlength="1000" rows="4" style="resize:vertical;width:100%;box-sizing:border-box;padding:0.65rem 0.9rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:1rem;outline:none;font-family:inherit;transition:border-color 0.2s;" onfocus="this.style.borderColor='var(--color-primary)'" onblur="this.style.borderColor='var(--color-secondary)'"></textarea>
|
||
<label>Datum & Uhrzeit *</label>
|
||
<input type="datetime-local" id="eventStartAt">
|
||
<div class="modal-footer">
|
||
<button class="btn" style="background:var(--color-secondary);color:var(--color-text);" onclick="closeEventModal()">Abbrechen</button>
|
||
<button class="btn" id="eventSubmitBtn" onclick="submitEvent()">Speichern</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Hinweis-Modal ──────────────────────────────────────────────────────── -->
|
||
<div class="modal-overlay" id="alertModal">
|
||
<div class="modal" style="width:min(380px,95vw);">
|
||
<p id="alertMessage" style="margin:0 0 1.25rem;font-size:0.95rem;"></p>
|
||
<div class="modal-footer">
|
||
<button class="btn" onclick="document.getElementById('alertModal').classList.remove('open')">OK</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Bestätigungs-Modal ─────────────────────────────────────────────────── -->
|
||
<div class="modal-overlay" id="confirmModal">
|
||
<div class="modal" style="width:min(380px,95vw);">
|
||
<h3 id="confirmTitle">Bestätigung</h3>
|
||
<p id="confirmMessage" style="color:var(--color-muted);font-size:0.92rem;margin:0 0 0.25rem;"></p>
|
||
<div class="modal-footer">
|
||
<button class="btn" style="background:var(--color-secondary);color:var(--color-text);" onclick="closeConfirm()">Abbrechen</button>
|
||
<button class="btn" id="confirmOkBtn" style="background:#c0392b;">Löschen</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Galerie Lightbox ───────────────────────────────────────────────────── -->
|
||
<div class="lb" id="lightbox" onclick="closeLightbox()">
|
||
<button class="lb-close" onclick="closeLightbox()">✕</button>
|
||
<button class="lb-nav lb-prev" id="lbPrev" onclick="event.stopPropagation();lbNav(-1)">‹</button>
|
||
<img id="lbImg" src="" alt="" onclick="event.stopPropagation()">
|
||
<button class="lb-nav lb-next" id="lbNext" onclick="event.stopPropagation();lbNav(1)">›</button>
|
||
</div>
|
||
|
||
<script src="/js/icons.js"></script>
|
||
<script src="/js/nav.js"></script>
|
||
<script>
|
||
const params = new URLSearchParams(location.search);
|
||
const locationId = params.get('id');
|
||
let locDetail = null;
|
||
let myUserId = null;
|
||
let isOwner = false;
|
||
let isAdmin = false;
|
||
let isFollowing = false;
|
||
|
||
// ── Bild-Resize ───────────────────────────────────────────────────────────────
|
||
function resizeImage(file, maxPx, quality) {
|
||
return new Promise((resolve, reject) => {
|
||
const img = new Image();
|
||
const url = URL.createObjectURL(file);
|
||
img.onload = () => {
|
||
URL.revokeObjectURL(url);
|
||
let w = img.naturalWidth, h = img.naturalHeight;
|
||
if (w > maxPx || h > maxPx) {
|
||
if (w >= h) { h = Math.max(1, Math.round(maxPx * h / w)); w = maxPx; }
|
||
else { w = Math.max(1, Math.round(maxPx * w / h)); h = maxPx; }
|
||
}
|
||
const canvas = document.createElement('canvas');
|
||
canvas.width = w; canvas.height = h;
|
||
canvas.getContext('2d').drawImage(img, 0, 0, w, h);
|
||
resolve(canvas.toDataURL('image/jpeg', quality || 0.85).split(',')[1]);
|
||
};
|
||
img.onerror = reject;
|
||
img.src = url;
|
||
});
|
||
}
|
||
|
||
function escHtml(s) {
|
||
return (s || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||
}
|
||
|
||
const DAY_NAMES = ['Montag','Dienstag','Mittwoch','Donnerstag','Freitag','Samstag','Sonntag'];
|
||
|
||
function formatDate(dt) {
|
||
if (!dt) return '';
|
||
const d = new Date(dt);
|
||
return d.toLocaleDateString('de-DE', { weekday:'short', day:'2-digit', month:'2-digit', year:'numeric' })
|
||
+ ', ' + d.toLocaleTimeString('de-DE', { hour:'2-digit', minute:'2-digit' }) + ' Uhr';
|
||
}
|
||
|
||
// ── Tabs ──────────────────────────────────────────────────────────────────────
|
||
const VALID_TABS = ['grunddaten', 'admins', 'posteingang', 'veranstaltungen'];
|
||
|
||
function switchTab(name) {
|
||
if (!VALID_TABS.includes(name)) name = 'grunddaten';
|
||
document.querySelectorAll('.tab-btn').forEach(btn =>
|
||
btn.classList.toggle('active', btn.dataset.tab === name));
|
||
document.querySelectorAll('.tab-panel').forEach(panel =>
|
||
panel.classList.toggle('active', panel.id === 'tab-' + name));
|
||
history.replaceState(null, '', location.pathname + location.search + '#' + name);
|
||
}
|
||
|
||
// ── Lade Seite ────────────────────────────────────────────────────────────────
|
||
async function loadPage() {
|
||
if (!locationId) { document.getElementById('content').innerHTML = '<p>Keine Location-ID angegeben.</p>'; return; }
|
||
|
||
const [meRes, locRes] = await Promise.all([
|
||
fetch('/login/me'),
|
||
fetch(`/locations/${locationId}`)
|
||
]);
|
||
if (!locRes.ok) { document.getElementById('content').innerHTML = '<p>Location nicht gefunden.</p>'; return; }
|
||
|
||
if (meRes.ok) {
|
||
const me = await meRes.json();
|
||
myUserId = me.userId;
|
||
}
|
||
|
||
locDetail = await locRes.json();
|
||
isOwner = locDetail.ownerId === myUserId;
|
||
isAdmin = isOwner || !!locDetail.isAdmin;
|
||
isFollowing = !!locDetail.following;
|
||
|
||
renderPage();
|
||
|
||
if (isAdmin) {
|
||
const chatWithId = new URLSearchParams(location.search).get('chatWith');
|
||
const hash = location.hash.replace('#', '');
|
||
const initialTab = chatWithId ? 'posteingang' : (VALID_TABS.includes(hash) ? hash : 'grunddaten');
|
||
switchTab(initialTab);
|
||
renderAdminList();
|
||
loadInbox();
|
||
loadEvents();
|
||
} else {
|
||
loadEvents();
|
||
}
|
||
}
|
||
|
||
function renderPage() {
|
||
const loc = locDetail;
|
||
const imgHtml = loc.profilePictureHq || loc.profilePictureLq
|
||
? `<img src="data:image/jpeg;base64,${loc.profilePictureHq || loc.profilePictureLq}" alt="${escHtml(loc.name)}">`
|
||
: '📍';
|
||
|
||
const ownerActions = isOwner ? `
|
||
<div class="owner-actions">
|
||
<button class="btn" style="font-size:0.85rem;" onclick="openEditModal()">✎ Bearbeiten</button>
|
||
<button class="btn" style="background:var(--color-secondary);color:var(--color-text);font-size:0.85rem;" onclick="deleteLocation()">Löschen</button>
|
||
</div>` : `
|
||
<div class="owner-actions">
|
||
<button class="btn" id="followBtn" style="font-size:0.85rem;${isFollowing ? 'background:var(--color-primary);color:#fff;' : 'background:var(--color-secondary);color:var(--color-text);'}" onclick="toggleFollow()">
|
||
${isFollowing ? '★ Abonniert' : '☆ Abonnieren'}
|
||
</button>
|
||
${loc.virtualUserId && myUserId && !isAdmin ? `<button class="btn" style="font-size:0.85rem;" onclick="contactLocation('${loc.virtualUserId}')">✉ Kontaktieren</button>` : ''}
|
||
</div>`;
|
||
|
||
let hoursHtml = '';
|
||
if (loc.openingHours && loc.openingHours.length > 0) {
|
||
const rowsHtml = loc.openingHours.map(h => {
|
||
const dayName = DAY_NAMES[h.dayOfWeek - 1] || '';
|
||
const timeText = h.closed
|
||
? '<span class="hours-closed">Geschlossen</span>'
|
||
: `${h.openTime || '--:--'} – ${h.closeTime || '--:--'}`;
|
||
return `<tr><td>${dayName}</td><td>${timeText}</td></tr>`;
|
||
}).join('');
|
||
hoursHtml = `
|
||
<div class="section-title">Öffnungszeiten</div>
|
||
<table class="hours-table"><tbody>${rowsHtml}</tbody></table>`;
|
||
}
|
||
|
||
const galleryHtml = buildGalleryHtml(loc.gallery || []);
|
||
|
||
const locHeaderHtml = `
|
||
<div class="loc-header">
|
||
<div class="loc-avatar">${imgHtml}</div>
|
||
<div class="loc-meta">
|
||
<div class="loc-name">${escHtml(loc.name)}</div>
|
||
${(loc.street || loc.city) ? `<div class="loc-city">📍 ${escHtml([loc.street, loc.city].filter(Boolean).join(', '))}</div>` : ''}
|
||
${loc.description ? `<div class="loc-desc">${escHtml(loc.description)}</div>` : ''}
|
||
${ownerActions}
|
||
</div>
|
||
</div>`;
|
||
|
||
const gallerySection = `
|
||
<div class="section-title">
|
||
Galerie
|
||
${isOwner ? `<label class="btn" style="font-size:0.8rem;cursor:pointer;">
|
||
+ Bild hinzufügen
|
||
<input type="file" accept="image/*" style="display:none;" onchange="uploadGalleryImage(this)">
|
||
</label>` : ''}
|
||
</div>
|
||
<div class="gallery-grid" id="galleryGrid">${galleryHtml}</div>`;
|
||
|
||
const eventsSection = `
|
||
<div class="section-title">
|
||
Veranstaltungen
|
||
${isOwner ? `<button class="btn" style="font-size:0.8rem;" onclick="openEventModal()">+ Veranstaltung erstellen</button>` : ''}
|
||
</div>
|
||
<div class="event-list" id="eventList"><p style="color:var(--color-muted);font-size:0.9rem;">Wird geladen…</p></div>
|
||
<div id="pastEventsSection" style="display:none;">
|
||
<div class="section-title" style="margin-top:1.5rem;">Vergangene Veranstaltungen</div>
|
||
<div class="event-list" id="pastEventList"></div>
|
||
</div>`;
|
||
|
||
if (isAdmin) {
|
||
document.getElementById('content').innerHTML = `
|
||
<div class="tab-bar">
|
||
<button class="tab-btn" data-tab="grunddaten" onclick="switchTab('grunddaten')">Grunddaten</button>
|
||
<button class="tab-btn" data-tab="admins" onclick="switchTab('admins')">Administrator*Innen</button>
|
||
<button class="tab-btn" data-tab="posteingang" onclick="switchTab('posteingang')">Posteingang</button>
|
||
<button class="tab-btn" data-tab="veranstaltungen" onclick="switchTab('veranstaltungen')">Veranstaltungen</button>
|
||
</div>
|
||
|
||
<div class="tab-panel" id="tab-grunddaten">
|
||
${locHeaderHtml}
|
||
${hoursHtml}
|
||
${gallerySection}
|
||
</div>
|
||
|
||
<div class="tab-panel" id="tab-admins">
|
||
<div id="adminList" style="display:flex;flex-direction:column;gap:0.5rem;margin-bottom:0.75rem;"></div>
|
||
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;">
|
||
<div style="flex:1;min-width:180px;">
|
||
<input type="text" id="adminSearchInput" placeholder="Mitglied suchen…" autocomplete="off"
|
||
oninput="onAdminSearch()" style="width:100%;box-sizing:border-box;">
|
||
</div>
|
||
<button class="btn" style="font-size:0.85rem;white-space:nowrap;" onclick="addAdminFromSearch()">+ Admin hinzufügen</button>
|
||
</div>
|
||
${isOwner ? `
|
||
<div style="margin-top:1rem;padding-top:1rem;border-top:1px solid var(--color-secondary);">
|
||
<div style="font-size:0.82rem;color:var(--color-muted);margin-bottom:0.5rem;">Inhaberrechte übertragen</div>
|
||
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;">
|
||
<div style="flex:1;min-width:180px;">
|
||
<input type="text" id="ownerSearchInput" placeholder="Mitglied suchen…" autocomplete="off"
|
||
oninput="onOwnerSearch()" style="width:100%;box-sizing:border-box;">
|
||
</div>
|
||
<button class="btn" style="font-size:0.85rem;white-space:nowrap;background:#c0392b;" onclick="transferOwner()">Inhaberwechsel</button>
|
||
</div>
|
||
</div>` : ''}
|
||
</div>
|
||
|
||
<div class="tab-panel" id="tab-posteingang">
|
||
<div id="inboxList" class="inbox-list"><p style="color:var(--color-muted);font-size:0.9rem;">Wird geladen…</p></div>
|
||
<div class="inbox-chat" id="inboxChat">
|
||
<div class="inbox-chat-header">
|
||
<button class="inbox-chat-back" onclick="closeInboxChat()" aria-label="Zurück">‹</button>
|
||
<span id="inboxChatName" style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"></span>
|
||
<button class="inbox-chat-close" onclick="closeInboxChat()" aria-label="Schließen">✕</button>
|
||
</div>
|
||
<div class="inbox-chat-messages" id="inboxChatMessages"></div>
|
||
<div class="inbox-lock-hint" id="inboxLockHint" style="display:none;"></div>
|
||
<div class="inbox-reply-trigger" id="inboxReplyTrigger" style="display:none;">
|
||
<button class="btn" onclick="startReplying()">✎ Antworten</button>
|
||
</div>
|
||
<div class="inbox-reply-area" id="inboxReplyArea" style="display:none;">
|
||
<input type="text" id="inboxReplyInput" placeholder="Antwort eingeben…" autocomplete="off"
|
||
onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendInboxReply();}">
|
||
<button class="btn inbox-reply-btn" onclick="sendInboxReply()">↑</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tab-panel" id="tab-veranstaltungen">
|
||
${eventsSection}
|
||
</div>`;
|
||
} else {
|
||
document.getElementById('content').innerHTML = `
|
||
${locHeaderHtml}
|
||
${hoursHtml}
|
||
${gallerySection}
|
||
${eventsSection}`;
|
||
}
|
||
}
|
||
|
||
function buildGalleryHtml(gallery) {
|
||
return gallery.map(img => `
|
||
<div class="gallery-img-wrap">
|
||
<img src="data:image/jpeg;base64,${img.imageData}" alt="Galeriebild"
|
||
onclick="openLightbox(this.src)">
|
||
${isOwner ? `<button class="gallery-del-btn" onclick="deleteGalleryImage('${img.imageId}')">✕</button>` : ''}
|
||
</div>`).join('');
|
||
}
|
||
|
||
// ── Galerie ────────────────────────────────────────────────────────────────────
|
||
async function uploadGalleryImage(input) {
|
||
const file = input.files[0];
|
||
if (!file) return;
|
||
try {
|
||
const imageData = await resizeImage(file, 1024, 0.88);
|
||
const res = await fetch(`/locations/${locationId}/gallery`, {
|
||
method: 'POST',
|
||
headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify({ imageData })
|
||
});
|
||
if (res.status === 422) { showAlert('Maximal 20 Galeriebilder erlaubt.'); return; }
|
||
if (!res.ok) throw new Error();
|
||
const img = await res.json();
|
||
const grid = document.getElementById('galleryGrid');
|
||
grid.insertAdjacentHTML('beforeend', `
|
||
<div class="gallery-img-wrap">
|
||
<img src="data:image/jpeg;base64,${img.imageData}" alt="Galeriebild" onclick="openLightbox(this.src)">
|
||
<button class="gallery-del-btn" onclick="deleteGalleryImage('${img.imageId}')">✕</button>
|
||
</div>`);
|
||
} catch { showAlert('Fehler beim Hochladen.'); }
|
||
input.value = '';
|
||
}
|
||
|
||
async function deleteGalleryImage(imageId) {
|
||
if (!confirm('Bild löschen?')) return;
|
||
const res = await fetch(`/locations/${locationId}/gallery/${imageId}`, { method: 'DELETE' });
|
||
if (!res.ok) { showAlert('Fehler beim Löschen.'); return; }
|
||
await loadPage();
|
||
}
|
||
|
||
// ── Events ─────────────────────────────────────────────────────────────────────
|
||
function buildEventCard(e, isPast) {
|
||
const imgHtml = e.imageData
|
||
? `<img src="data:image/jpeg;base64,${e.imageData}" alt="${escHtml(e.title)}">`
|
||
: '🗓';
|
||
const opacity = isPast ? 'opacity:0.6;' : '';
|
||
return `
|
||
<a class="event-card" href="/community/event-detail.html?id=${e.eventId}" style="${opacity}">
|
||
<div class="event-card-img">${imgHtml}</div>
|
||
<div class="event-card-body">
|
||
<div class="event-card-title">${escHtml(e.title)}</div>
|
||
<div class="event-card-date">${formatDate(e.startAt)}</div>
|
||
<div class="event-card-attendees">${e.attendeeCount} Teilnehmer*in(nen)${e.attendingMe ? ' · Du nimmst teil' : ''}</div>
|
||
</div>
|
||
</a>`;
|
||
}
|
||
|
||
async function loadEvents() {
|
||
const res = await fetch(`/locations/${locationId}/events`);
|
||
if (!res.ok) return;
|
||
const events = await res.json();
|
||
const list = document.getElementById('eventList');
|
||
if (!list) return;
|
||
|
||
const now = new Date();
|
||
const future = events.filter(e => new Date(e.startAt) >= now);
|
||
const past = events.filter(e => new Date(e.startAt) < now)
|
||
.slice(-5) // letzte 5
|
||
.reverse(); // neueste zuerst
|
||
|
||
list.innerHTML = future.length
|
||
? future.map(e => buildEventCard(e, false)).join('')
|
||
: '<p style="color:var(--color-muted);font-size:0.9rem;">Keine bevorstehenden Veranstaltungen.</p>';
|
||
|
||
const pastSection = document.getElementById('pastEventsSection');
|
||
if (past.length && pastSection) {
|
||
document.getElementById('pastEventList').innerHTML = past.map(e => buildEventCard(e, true)).join('');
|
||
pastSection.style.display = '';
|
||
} else if (pastSection) {
|
||
pastSection.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// ── Lightbox ───────────────────────────────────────────────────────────────────
|
||
let lbSrcs = [], lbIdx = 0;
|
||
|
||
function openLightbox(src) {
|
||
lbSrcs = Array.from(document.querySelectorAll('#galleryGrid .gallery-img-wrap img')).map(i => i.src);
|
||
lbIdx = Math.max(0, lbSrcs.indexOf(src));
|
||
lbShow();
|
||
document.getElementById('lightbox').classList.add('open');
|
||
}
|
||
function lbShow() {
|
||
document.getElementById('lbImg').src = lbSrcs[lbIdx];
|
||
document.getElementById('lbPrev').disabled = lbIdx === 0;
|
||
document.getElementById('lbNext').disabled = lbIdx === lbSrcs.length - 1;
|
||
}
|
||
function lbNav(dir) {
|
||
lbIdx = Math.max(0, Math.min(lbSrcs.length - 1, lbIdx + dir));
|
||
lbShow();
|
||
}
|
||
function closeLightbox() { document.getElementById('lightbox').classList.remove('open'); }
|
||
document.addEventListener('keydown', e => {
|
||
if (!document.getElementById('lightbox').classList.contains('open')) return;
|
||
if (e.key === 'Escape') closeLightbox();
|
||
else if (e.key === 'ArrowLeft') lbNav(-1);
|
||
else if (e.key === 'ArrowRight') lbNav(1);
|
||
});
|
||
|
||
// ── Edit Modal ─────────────────────────────────────────────────────────────────
|
||
let _editLq = null, _editHq = null, _editLat = null, _editLon = null, _editStreet = null, _editCity = null, _editCityTimer = null;
|
||
|
||
function buildHoursGrid(gridId, existing) {
|
||
const grid = document.getElementById(gridId);
|
||
grid.innerHTML = '';
|
||
const byDay = {};
|
||
(existing || []).forEach(h => { byDay[h.dayOfWeek] = h; });
|
||
DAY_NAMES.forEach((name, i) => {
|
||
const day = i + 1;
|
||
const h = byDay[day] || {};
|
||
grid.insertAdjacentHTML('beforeend', `
|
||
<span>${name}</span>
|
||
<input type="time" id="open_${day}_${gridId}" value="${h.openTime || ''}" style="font-size:0.82rem;padding:0.25rem 0.4rem;">
|
||
<input type="time" id="close_${day}_${gridId}" value="${h.closeTime || ''}" style="font-size:0.82rem;padding:0.25rem 0.4rem;">
|
||
<label style="display:flex;align-items:center;gap:0.25rem;font-size:0.82rem;white-space:nowrap;">
|
||
<input type="checkbox" id="closed_${day}_${gridId}" ${h.closed ? 'checked' : ''}> Geschlossen
|
||
</label>
|
||
`);
|
||
});
|
||
}
|
||
|
||
function collectHours(gridId) {
|
||
const result = [];
|
||
for (let d = 1; d <= 7; d++) {
|
||
const open = document.getElementById(`open_${d}_${gridId}`)?.value;
|
||
const close = document.getElementById(`close_${d}_${gridId}`)?.value;
|
||
const closed = document.getElementById(`closed_${d}_${gridId}`)?.checked;
|
||
if (open || close || closed) {
|
||
result.push({ dayOfWeek: d, openTime: open || null, closeTime: close || null, closed: !!closed });
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
function openEditModal() {
|
||
const loc = locDetail;
|
||
_editLq = null; _editHq = null;
|
||
_editLat = loc.lat; _editLon = loc.lon;
|
||
_editStreet = loc.street || null; _editCity = loc.city || null;
|
||
document.getElementById('editName').value = loc.name || '';
|
||
document.getElementById('editDesc').value = loc.description || '';
|
||
const cityInp = document.getElementById('editCity');
|
||
const addressLabel = [loc.street, loc.city].filter(Boolean).join(', ');
|
||
cityInp.value = addressLabel;
|
||
cityInp.readOnly = !!addressLabel;
|
||
document.getElementById('editCityClear').style.display = addressLabel ? '' : 'none';
|
||
document.getElementById('editLocMsg').textContent = '';
|
||
const picSrc = loc.profilePictureHq || loc.profilePictureLq;
|
||
document.getElementById('editPicPreview').innerHTML = picSrc
|
||
? `<img src="data:image/jpeg;base64,${picSrc}" alt="Vorschau">`
|
||
: '📍';
|
||
buildHoursGrid('editHoursGrid', loc.openingHours || []);
|
||
document.getElementById('editModal').classList.add('open');
|
||
}
|
||
function closeEditModal() { document.getElementById('editModal').classList.remove('open'); }
|
||
|
||
async function onEditPicChange(input) {
|
||
const file = input.files[0]; if (!file) return;
|
||
_editLq = await resizeImage(file, 120, 0.75);
|
||
_editHq = await resizeImage(file, 1024, 0.88);
|
||
document.getElementById('editPicPreview').innerHTML = `<img src="data:image/jpeg;base64,${_editHq}" alt="Vorschau">`;
|
||
}
|
||
|
||
function onEditCityInput() {
|
||
const q = document.getElementById('editCity').value.trim();
|
||
_editLat = null; _editLon = null; _editStreet = null; _editCity = null;
|
||
document.getElementById('editCityClear').style.display = 'none';
|
||
clearTimeout(_editCityTimer);
|
||
if (q.length < 2) { document.getElementById('editCitySuggestions').style.display = 'none'; return; }
|
||
_editCityTimer = setTimeout(() => fetchAddressSuggestions(q), 300);
|
||
}
|
||
|
||
function fmtAddress(r) {
|
||
const road = r.address.road || r.address.pedestrian || r.address.path || '';
|
||
const hn = r.address.house_number || '';
|
||
const street = (road + (hn ? ' ' + hn : '')).trim();
|
||
const plz = r.address.postcode || '';
|
||
const city = r.address.city || r.address.town || r.address.village || r.address.county || '';
|
||
const parts = [];
|
||
if (street) parts.push(street);
|
||
const cityPart = (plz && city) ? plz + ' ' + city : (plz || city);
|
||
if (cityPart) parts.push(cityPart);
|
||
return { label: parts.join(', '), street, city };
|
||
}
|
||
|
||
async function fetchAddressSuggestions(q) {
|
||
try {
|
||
const url = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(q)}&format=json&addressdetails=1&limit=5`;
|
||
const res = await fetch(url, { headers: { 'Accept-Language': 'de' } });
|
||
if (!res.ok) return;
|
||
const results = await res.json();
|
||
const ul = document.getElementById('editCitySuggestions');
|
||
if (!results.length) { ul.style.display = 'none'; return; }
|
||
ul.innerHTML = results.map(r => {
|
||
const { label, street, city } = fmtAddress(r);
|
||
const esc = s => s.replace(/\\/g,'\\\\').replace(/'/g,"\\'");
|
||
return `<li style="padding:0.5rem 0.75rem;cursor:pointer;font-size:0.9rem;border-bottom:1px solid var(--color-secondary);"
|
||
onmousedown="selectEditAddress(event,'${esc(label)}','${esc(street)}','${esc(city)}',${parseFloat(r.lat)},${parseFloat(r.lon)})">${label}</li>`;
|
||
}).join('');
|
||
ul.style.display = '';
|
||
} catch (_) {}
|
||
}
|
||
|
||
function selectEditAddress(e, label, street, city, lat, lon) {
|
||
e.preventDefault();
|
||
const inp = document.getElementById('editCity');
|
||
inp.value = label; inp.readOnly = true;
|
||
_editStreet = street || null;
|
||
_editCity = city || null;
|
||
_editLat = lat; _editLon = lon;
|
||
document.getElementById('editCityClear').style.display = '';
|
||
document.getElementById('editCitySuggestions').style.display = 'none';
|
||
document.getElementById('editLocMsg').textContent = '';
|
||
}
|
||
|
||
function clearEditCity() {
|
||
const inp = document.getElementById('editCity');
|
||
inp.value = ''; inp.readOnly = false;
|
||
_editLat = null; _editLon = null; _editStreet = null; _editCity = null;
|
||
document.getElementById('editCityClear').style.display = 'none';
|
||
document.getElementById('editLocMsg').textContent = '';
|
||
inp.focus();
|
||
}
|
||
|
||
document.addEventListener('click', e => {
|
||
if (!e.target.closest('#editStadtRow')) document.getElementById('editCitySuggestions').style.display = 'none';
|
||
});
|
||
|
||
async function submitEdit() {
|
||
const name = document.getElementById('editName').value.trim();
|
||
if (!name) { showAlert('Name darf nicht leer sein.'); return; }
|
||
const addrVal = document.getElementById('editCity').value.trim();
|
||
if (addrVal && _editLat == null) {
|
||
document.getElementById('editLocMsg').textContent = 'Bitte eine Adresse aus der Vorschlagsliste auswählen oder per GPS ermitteln.';
|
||
document.getElementById('editCity').focus();
|
||
return;
|
||
}
|
||
|
||
const btn = document.getElementById('editSubmitBtn');
|
||
btn.disabled = true; btn.textContent = 'Wird gespeichert…';
|
||
|
||
try {
|
||
const body = {
|
||
name,
|
||
description: document.getElementById('editDesc').value.trim() || null,
|
||
street: _editStreet,
|
||
city: _editCity,
|
||
lat: _editLat,
|
||
lon: _editLon
|
||
};
|
||
if (_editLq) body.profilePictureLq = _editLq;
|
||
if (_editHq) body.profilePictureHq = _editHq;
|
||
|
||
const res = await fetch(`/locations/${locationId}`, {
|
||
method: 'PUT',
|
||
headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify(body)
|
||
});
|
||
if (!res.ok) throw new Error();
|
||
|
||
const hours = collectHours('editHoursGrid');
|
||
await fetch(`/locations/${locationId}/opening-hours`, {
|
||
method: 'PUT',
|
||
headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify(hours)
|
||
});
|
||
|
||
locDetail = await (await fetch(`/locations/${locationId}`)).json();
|
||
closeEditModal();
|
||
renderPage();
|
||
loadEvents();
|
||
} catch { showAlert('Fehler beim Speichern.'); }
|
||
finally { btn.disabled = false; btn.textContent = 'Speichern'; }
|
||
}
|
||
|
||
async function toggleFollow() {
|
||
const btn = document.getElementById('followBtn');
|
||
if (btn) btn.disabled = true;
|
||
try {
|
||
const res = await fetch(`/locations/${locationId}/follow`, { method: 'POST' });
|
||
if (!res.ok) throw new Error();
|
||
const data = await res.json();
|
||
isFollowing = data.following;
|
||
if (btn) {
|
||
btn.textContent = isFollowing ? '★ Abonniert' : '☆ Abonnieren';
|
||
btn.style.background = isFollowing ? 'var(--color-primary)' : 'var(--color-secondary)';
|
||
btn.style.color = isFollowing ? '#fff' : 'var(--color-text)';
|
||
}
|
||
} catch (_) { showAlert('Fehler beim Aktualisieren des Abonnements.'); }
|
||
finally { if (btn) btn.disabled = false; }
|
||
}
|
||
|
||
async function deleteLocation() {
|
||
if (!confirm('Location wirklich löschen? Alle Veranstaltungen und Galeriebilder werden ebenfalls gelöscht.')) return;
|
||
const res = await fetch(`/locations/${locationId}`, { method: 'DELETE' });
|
||
if (!res.ok) { showAlert('Fehler beim Löschen.'); return; }
|
||
window.location.href = '/community/locations.html';
|
||
}
|
||
|
||
// ── Event Modal ────────────────────────────────────────────────────────────────
|
||
let _evtImg = null, _editEventId = null;
|
||
|
||
function openEventModal(evtId) {
|
||
_editEventId = evtId || null;
|
||
_evtImg = null;
|
||
document.getElementById('eventModalTitle').textContent = evtId ? 'Veranstaltung bearbeiten' : 'Veranstaltung erstellen';
|
||
document.getElementById('eventTitle').value = '';
|
||
document.getElementById('eventDesc').value = '';
|
||
document.getElementById('eventStartAt').value = '';
|
||
// Nur Termine in der Zukunft erlauben
|
||
const nowLocal = new Date(Date.now() - new Date().getTimezoneOffset() * 60000).toISOString().slice(0, 16);
|
||
document.getElementById('eventStartAt').min = nowLocal;
|
||
document.getElementById('eventPicPreview').innerHTML = '🗓';
|
||
document.getElementById('eventModal').classList.add('open');
|
||
}
|
||
function closeEventModal() { document.getElementById('eventModal').classList.remove('open'); }
|
||
|
||
async function onEventPicChange(input) {
|
||
const file = input.files[0]; if (!file) return;
|
||
_evtImg = await resizeImage(file, 1024, 0.88);
|
||
document.getElementById('eventPicPreview').innerHTML = `<img src="data:image/jpeg;base64,${_evtImg}" alt="Vorschau">`;
|
||
}
|
||
|
||
async function submitEvent() {
|
||
const title = document.getElementById('eventTitle').value.trim();
|
||
const startAt = document.getElementById('eventStartAt').value;
|
||
if (!title) { showAlert('Bitte gib einen Titel ein.'); return; }
|
||
if (!startAt) { showAlert('Bitte wähle Datum und Uhrzeit.'); return; }
|
||
if (new Date(startAt) <= new Date()) { showAlert('Der Termin muss in der Zukunft liegen.'); return; }
|
||
|
||
const btn = document.getElementById('eventSubmitBtn');
|
||
btn.disabled = true; btn.textContent = 'Wird gespeichert…';
|
||
|
||
try {
|
||
const body = {
|
||
title,
|
||
description: document.getElementById('eventDesc').value.trim() || null,
|
||
imageData: _evtImg,
|
||
startAt: startAt + ':00'
|
||
};
|
||
|
||
const url = _editEventId
|
||
? `/locations/${locationId}/events/${_editEventId}`
|
||
: `/locations/${locationId}/events`;
|
||
const method = _editEventId ? 'PUT' : 'POST';
|
||
|
||
const res = await fetch(url, {
|
||
method,
|
||
headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify(body)
|
||
});
|
||
if (!res.ok) throw new Error();
|
||
|
||
closeEventModal();
|
||
loadEvents();
|
||
} catch { showAlert('Fehler beim Speichern.'); }
|
||
finally { btn.disabled = false; btn.textContent = 'Speichern'; }
|
||
}
|
||
|
||
function showAlert(message) {
|
||
document.getElementById('alertMessage').textContent = message;
|
||
document.getElementById('alertModal').classList.add('open');
|
||
}
|
||
|
||
function openConfirm(title, message, onOk) {
|
||
document.getElementById('confirmTitle').textContent = title;
|
||
document.getElementById('confirmMessage').textContent = message;
|
||
const btn = document.getElementById('confirmOkBtn');
|
||
btn.onclick = () => { closeConfirm(); onOk(); };
|
||
document.getElementById('confirmModal').classList.add('open');
|
||
}
|
||
function closeConfirm() { document.getElementById('confirmModal').classList.remove('open'); }
|
||
|
||
// ── Admin-Verwaltung ──────────────────────────────────────────────────────────
|
||
let _adminSearchSelected = null;
|
||
let _ownerSearchSelected = null;
|
||
let _adminSearchTimer = null;
|
||
let _ownerSearchTimer = null;
|
||
|
||
function renderAdminList() {
|
||
const list = document.getElementById('adminList');
|
||
if (!list || !locDetail.admins) return;
|
||
list.innerHTML = locDetail.admins.map(a => {
|
||
const pic = a.profilePicture
|
||
? `<img src="data:image/jpeg;base64,${a.profilePicture}" alt="" style="width:100%;height:100%;object-fit:cover;border-radius:50%;">`
|
||
: '◉';
|
||
const badge = a.isOwner
|
||
? `<span style="font-size:0.7rem;background:var(--color-primary);color:#fff;border-radius:4px;padding:0.1rem 0.4rem;margin-left:0.4rem;">Inhaber</span>`
|
||
: '';
|
||
const removeBtn = isAdmin && !a.isOwner
|
||
? `<button onclick="removeAdmin('${a.userId}')" style="margin-left:auto;background:none;border:none;color:var(--color-muted);cursor:pointer;font-size:1rem;padding:0.2rem 0.4rem;" title="Entfernen">✕</button>`
|
||
: '';
|
||
return `<div style="display:flex;align-items:center;gap:0.6rem;padding:0.4rem 0;">
|
||
<div style="width:32px;height:32px;border-radius:50%;background:var(--color-secondary);display:flex;align-items:center;justify-content:center;font-size:1rem;overflow:hidden;flex-shrink:0;">${pic}</div>
|
||
<span style="font-size:0.9rem;">${escHtml(a.name)}</span>${badge}${removeBtn}
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
async function removeAdmin(userId) {
|
||
const res = await fetch(`/locations/${locationId}/admins/${userId}`, { method: 'DELETE' });
|
||
if (!res.ok) { showAlert('Fehler beim Entfernen.'); return; }
|
||
locDetail.admins = locDetail.admins.filter(a => a.userId !== userId);
|
||
renderAdminList();
|
||
}
|
||
|
||
function onAdminSearch() {
|
||
const q = document.getElementById('adminSearchInput').value.trim();
|
||
_adminSearchSelected = null;
|
||
clearTimeout(_adminSearchTimer);
|
||
if (q.length < 2) { document.getElementById('adminSearchList').style.display = 'none'; return; }
|
||
_adminSearchTimer = setTimeout(() => fetchUserSuggestions(q, 'adminSearchList', sel => { _adminSearchSelected = sel; }), 250);
|
||
}
|
||
|
||
function onOwnerSearch() {
|
||
const q = document.getElementById('ownerSearchInput').value.trim();
|
||
_ownerSearchSelected = null;
|
||
clearTimeout(_ownerSearchTimer);
|
||
if (q.length < 2) { document.getElementById('ownerSearchList').style.display = 'none'; return; }
|
||
_ownerSearchTimer = setTimeout(() => fetchUserSuggestions(q, 'ownerSearchList', sel => { _ownerSearchSelected = sel; }), 250);
|
||
}
|
||
|
||
const _userCache = {};
|
||
|
||
async function fetchUserSuggestions(q, listId, onSelect) {
|
||
try {
|
||
const res = await fetch(`/social/users/search?q=${encodeURIComponent(q)}`);
|
||
if (!res.ok) return;
|
||
const users = await res.json();
|
||
const ul = document.getElementById(listId);
|
||
if (!users.length) { ul.style.display = 'none'; return; }
|
||
users.forEach(u => { _userCache[u.userId] = u; });
|
||
ul.innerHTML = users.map(u => `
|
||
<li style="padding:0.45rem 0.75rem;cursor:pointer;font-size:0.9rem;border-bottom:1px solid var(--color-secondary);"
|
||
onmousedown="event.preventDefault();selectUserSuggestion('${listId}','${u.userId}')">
|
||
${escHtml(u.name)}
|
||
</li>`).join('');
|
||
ul.__selectFn = onSelect;
|
||
// Dropdown unter dem zugehörigen Input positionieren
|
||
const inputId = listId === 'adminSearchList' ? 'adminSearchInput' : 'ownerSearchInput';
|
||
const rect = document.getElementById(inputId).getBoundingClientRect();
|
||
ul.style.top = (rect.bottom + 2) + 'px';
|
||
ul.style.left = rect.left + 'px';
|
||
ul.style.width = rect.width + 'px';
|
||
ul.style.display = '';
|
||
} catch(_) {}
|
||
}
|
||
|
||
function selectUserSuggestion(listId, userId) {
|
||
const ul = document.getElementById(listId);
|
||
const inputId = listId === 'adminSearchList' ? 'adminSearchInput' : 'ownerSearchInput';
|
||
const user = _userCache[userId];
|
||
if (!user) return;
|
||
document.getElementById(inputId).value = user.name;
|
||
ul.style.display = 'none';
|
||
if (ul.__selectFn) ul.__selectFn(user);
|
||
}
|
||
|
||
document.addEventListener('click', e => {
|
||
['adminSearchList','ownerSearchList'].forEach(id => {
|
||
const ul = document.getElementById(id);
|
||
if (ul && !e.target.closest('#' + id) && e.target.id !== (id === 'adminSearchList' ? 'adminSearchInput' : 'ownerSearchInput'))
|
||
ul.style.display = 'none';
|
||
});
|
||
});
|
||
|
||
async function addAdminFromSearch() {
|
||
if (!_adminSearchSelected) { showAlert('Bitte wähle ein Mitglied aus der Liste.'); return; }
|
||
const res = await fetch(`/locations/${locationId}/admins`, {
|
||
method: 'POST',
|
||
headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify({ userId: _adminSearchSelected.userId })
|
||
});
|
||
if (res.status === 409) { showAlert('Diese Person ist bereits Admin.'); return; }
|
||
if (!res.ok) { showAlert('Fehler beim Hinzufügen.'); return; }
|
||
const added = await res.json();
|
||
locDetail.admins = [...(locDetail.admins || []), added];
|
||
renderAdminList();
|
||
document.getElementById('adminSearchInput').value = '';
|
||
_adminSearchSelected = null;
|
||
}
|
||
|
||
async function transferOwner() {
|
||
if (!_ownerSearchSelected) { showAlert('Bitte wähle ein Mitglied aus der Liste.'); return; }
|
||
openConfirm(
|
||
'Inhaberwechsel',
|
||
`Inhaberrechte wirklich an „${_ownerSearchSelected.name}" übertragen? Diese Aktion kann nicht rückgängig gemacht werden.`,
|
||
async () => {
|
||
const res = await fetch(`/locations/${locationId}/admins/transfer-owner`, {
|
||
method: 'PUT',
|
||
headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify({ userId: _ownerSearchSelected.userId })
|
||
});
|
||
if (!res.ok) { showAlert('Fehler beim Inhaberwechsel.'); return; }
|
||
// Seite neu laden – aktueller User ist jetzt kein Inhaber mehr
|
||
await loadPage();
|
||
}
|
||
);
|
||
}
|
||
|
||
// ── Kontaktieren ──────────────────────────────────────────────────────────────
|
||
function contactLocation(virtualUserId) {
|
||
window.location.href = '/community/nachrichten.html?partnerId=' + virtualUserId;
|
||
}
|
||
|
||
// ── Posteingang (Admin) ───────────────────────────────────────────────────────
|
||
let _inboxPartnerId = null;
|
||
|
||
async function loadInbox() {
|
||
const el = document.getElementById('inboxList');
|
||
if (!el) return;
|
||
try {
|
||
const res = await fetch(`/locations/${locationId}/inbox`);
|
||
if (!res.ok) { el.innerHTML = '<p style="color:var(--color-muted);font-size:0.9rem;">Nicht verfügbar.</p>'; return; }
|
||
const items = await res.json();
|
||
if (items.length === 0) {
|
||
el.innerHTML = '<p style="color:var(--color-muted);font-size:0.9rem;">Noch keine Nachrichten.</p>';
|
||
return;
|
||
}
|
||
el.innerHTML = items.map(item => {
|
||
const av = item.senderPicture
|
||
? `<img src="data:image/png;base64,${item.senderPicture}" alt="">`
|
||
: '◉';
|
||
const unreadHtml = item.unreadCount > 0
|
||
? `<span class="inbox-unread">${item.unreadCount}</span>` : '';
|
||
return `<div class="inbox-item" onclick="openInboxChat('${item.senderId}','${escHtml(item.senderName)}')">
|
||
<div class="inbox-avatar">${av}</div>
|
||
<div class="inbox-info">
|
||
<div class="inbox-name">${escHtml(item.senderName)}</div>
|
||
<div class="inbox-preview">${escHtml(item.lastMessage)}</div>
|
||
</div>
|
||
${unreadHtml}
|
||
</div>`;
|
||
}).join('');
|
||
|
||
// URL-Parameter: Chat direkt öffnen (z.B. nach Klick auf Benachrichtigung)
|
||
const chatWithId = new URLSearchParams(location.search).get('chatWith');
|
||
if (chatWithId && !_inboxPartnerId) {
|
||
const match = items.find(i => i.senderId === chatWithId);
|
||
if (match) openInboxChat(match.senderId, match.senderName);
|
||
}
|
||
} catch { el.innerHTML = '<p style="color:var(--color-muted);font-size:0.9rem;">Fehler beim Laden.</p>'; }
|
||
}
|
||
|
||
async function openInboxChat(partnerId, partnerName) {
|
||
_inboxPartnerId = partnerId;
|
||
document.getElementById('inboxChatName').textContent = partnerName;
|
||
document.getElementById('inboxChatMessages').innerHTML = '<p style="color:var(--color-muted);font-size:0.85rem;text-align:center;">Wird geladen…</p>';
|
||
document.getElementById('inboxChat').classList.add('open');
|
||
document.getElementById('inboxReplyInput').value = '';
|
||
// Eingabebereich zurücksetzen
|
||
document.getElementById('inboxReplyTrigger').style.display = 'none';
|
||
document.getElementById('inboxReplyArea').style.display = 'none';
|
||
document.getElementById('inboxLockHint').style.display = 'none';
|
||
|
||
const virtualId = locDetail.virtualUserId;
|
||
try {
|
||
const res = await fetch(`/locations/${locationId}/inbox/${partnerId}`);
|
||
if (!res.ok) throw new Error();
|
||
const data = await res.json();
|
||
const messages = data.messages || [];
|
||
|
||
const container = document.getElementById('inboxChatMessages');
|
||
container.innerHTML = '';
|
||
if (messages.length === 0) {
|
||
container.innerHTML = '<p style="color:var(--color-muted);font-size:0.85rem;text-align:center;">Noch keine Nachrichten.</p>';
|
||
} else {
|
||
messages.forEach(m => {
|
||
const isLocationSender = m.senderId === virtualId;
|
||
const wrap = document.createElement('div');
|
||
wrap.className = 'inbox-bubble-wrap ' + (isLocationSender ? 'me' : 'them');
|
||
const time = new Date(m.sentAt).toLocaleTimeString('de', { hour: '2-digit', minute: '2-digit' });
|
||
wrap.innerHTML = `<div class="inbox-bubble">${escHtml(m.text)}</div><div class="inbox-bubble-time">${time}</div>`;
|
||
container.appendChild(wrap);
|
||
});
|
||
}
|
||
requestAnimationFrame(() => { container.scrollTop = container.scrollHeight; });
|
||
|
||
// Lock-Status anzeigen
|
||
applyLockUi(data);
|
||
loadInbox();
|
||
} catch {
|
||
document.getElementById('inboxChatMessages').innerHTML = '<p style="color:var(--color-muted);font-size:0.85rem;text-align:center;">Fehler beim Laden.</p>';
|
||
}
|
||
}
|
||
|
||
function applyLockUi(data) {
|
||
const trigger = document.getElementById('inboxReplyTrigger');
|
||
const area = document.getElementById('inboxReplyArea');
|
||
const lockHint = document.getElementById('inboxLockHint');
|
||
|
||
if (data.lockedByMe) {
|
||
// Ich habe die Sperre → Eingabefeld direkt zeigen
|
||
trigger.style.display = 'none';
|
||
area.style.display = '';
|
||
lockHint.style.display = 'none';
|
||
document.getElementById('inboxReplyInput').focus();
|
||
} else if (data.canReply) {
|
||
// Frei → "Antworten"-Button zeigen
|
||
trigger.style.display = '';
|
||
area.style.display = 'none';
|
||
lockHint.style.display = 'none';
|
||
} else {
|
||
// Gesperrt durch anderen Admin
|
||
trigger.style.display = 'none';
|
||
area.style.display = 'none';
|
||
lockHint.style.display = '';
|
||
lockHint.textContent = `Wird gerade von ${data.lockedByName || 'einem anderen Admin'} beantwortet.`;
|
||
}
|
||
}
|
||
|
||
async function startReplying() {
|
||
if (!_inboxPartnerId) return;
|
||
try {
|
||
const res = await fetch(`/locations/${locationId}/inbox/${_inboxPartnerId}/lock`, {
|
||
method: 'POST'
|
||
});
|
||
if (res.status === 409) {
|
||
const body = await res.json();
|
||
const name = body.lockedByName || 'einem anderen Admin';
|
||
document.getElementById('inboxReplyTrigger').style.display = 'none';
|
||
document.getElementById('inboxLockHint').style.display = '';
|
||
document.getElementById('inboxLockHint').textContent =
|
||
`Wird gerade von ${name} beantwortet.`;
|
||
return;
|
||
}
|
||
if (!res.ok) { showAlert('Sperre konnte nicht erworben werden.'); return; }
|
||
document.getElementById('inboxReplyTrigger').style.display = 'none';
|
||
document.getElementById('inboxReplyArea').style.display = '';
|
||
document.getElementById('inboxReplyInput').focus();
|
||
} catch { showAlert('Fehler beim Sperren.'); }
|
||
}
|
||
|
||
function closeInboxChat() {
|
||
_inboxPartnerId = null;
|
||
document.getElementById('inboxChat').classList.remove('open');
|
||
document.getElementById('inboxReplyTrigger').style.display = 'none';
|
||
document.getElementById('inboxReplyArea').style.display = 'none';
|
||
document.getElementById('inboxLockHint').style.display = 'none';
|
||
loadInbox();
|
||
}
|
||
|
||
async function sendInboxReply() {
|
||
if (!_inboxPartnerId) return;
|
||
const input = document.getElementById('inboxReplyInput');
|
||
const text = input.value.trim();
|
||
if (!text) return;
|
||
input.value = '';
|
||
try {
|
||
const res = await fetch(`/locations/${locationId}/inbox/${_inboxPartnerId}/reply`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ text })
|
||
});
|
||
if (res.status === 409) {
|
||
const body = await res.json().catch(() => ({}));
|
||
const name = body.lockedByName || 'einem anderen Admin';
|
||
input.value = text;
|
||
// Eingabe sperren, Hinweis zeigen
|
||
document.getElementById('inboxReplyArea').style.display = 'none';
|
||
document.getElementById('inboxLockHint').style.display = '';
|
||
document.getElementById('inboxLockHint').textContent =
|
||
`Wird gerade von ${name} beantwortet. Deine Nachricht wurde nicht gesendet.`;
|
||
return;
|
||
}
|
||
if (!res.ok) { showAlert('Fehler beim Senden.'); input.value = text; return; }
|
||
// Konversation neu laden (Lock ist nach Senden bei mir)
|
||
const partnerName = document.getElementById('inboxChatName').textContent;
|
||
await openInboxChat(_inboxPartnerId, partnerName);
|
||
} catch { showAlert('Fehler beim Senden.'); input.value = text; }
|
||
}
|
||
|
||
// ── Init ──────────────────────────────────────────────────────────────────────
|
||
loadPage();
|
||
</script>
|
||
</body>
|
||
</html>
|