553 lines
26 KiB
PHP
553 lines
26 KiB
PHP
<?php
|
||
$footerPages = get_db()->query("SELECT title, slug FROM site_pages WHERE is_active=1 AND show_in_nav=1 ORDER BY sort_order, title")->fetchAll();
|
||
|
||
$privacyLabel = get_setting('footer_privacy_label', 'Datenschutz');
|
||
$privacyUrl = get_setting('footer_privacy_url', '');
|
||
$privacyHtml = get_setting('footer_privacy_html', '');
|
||
|
||
$imprintLabel = get_setting('footer_imprint_label', 'Impressum');
|
||
$imprintUrl = get_setting('footer_imprint_url', '');
|
||
$imprintHtml = get_setting('footer_imprint_html', '');
|
||
|
||
$hasPrivacy = $privacyUrl || $privacyHtml;
|
||
$hasImprint = $imprintUrl || $imprintHtml;
|
||
$hasPopup = (!$privacyUrl && $privacyHtml) || (!$imprintUrl && $imprintHtml);
|
||
?>
|
||
</div><!-- /.page-content -->
|
||
|
||
<?php if ($hasPopup): ?>
|
||
<!-- Popup-Overlay für Datenschutz / Impressum -->
|
||
<div id="legal-overlay" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,.55); z-index:9999; overflow-y:auto; padding:40px 16px">
|
||
<div style="background:#fff; max-width:760px; margin:0 auto; border-radius:10px; padding:32px 36px; position:relative; font-size:.9rem; line-height:1.7">
|
||
<button onclick="closeLegalPopup()" style="position:absolute; top:14px; right:18px; background:none; border:none; font-size:1.4rem; cursor:pointer; color:#6b7280">×</button>
|
||
<div id="legal-popup-content"></div>
|
||
</div>
|
||
</div>
|
||
<script>
|
||
function openLegalPopup(html) {
|
||
document.getElementById('legal-popup-content').innerHTML = html;
|
||
document.getElementById('legal-overlay').style.display = 'block';
|
||
document.body.style.overflow = 'hidden';
|
||
}
|
||
function closeLegalPopup() {
|
||
document.getElementById('legal-overlay').style.display = 'none';
|
||
document.body.style.overflow = '';
|
||
}
|
||
document.getElementById('legal-overlay').addEventListener('click', function(e){
|
||
if (e.target === this) closeLegalPopup();
|
||
});
|
||
document.addEventListener('keydown', function(e){ if(e.key==='Escape') closeLegalPopup(); });
|
||
</script>
|
||
<?php endif; ?>
|
||
|
||
<footer class="public-footer">
|
||
<nav style="margin-bottom:8px; display:flex; gap:20px; justify-content:center; flex-wrap:wrap">
|
||
<?php if ($footerPages): ?>
|
||
<?php foreach ($footerPages as $fp): ?>
|
||
<a href="/page.php?slug=<?= e($fp['slug']) ?>" style="color:#6b7280; font-size:.78rem"><?= e($fp['title']) ?></a>
|
||
<?php endforeach; ?>
|
||
<?php endif; ?>
|
||
|
||
<?php if ($hasPrivacy): ?>
|
||
<?php if ($privacyUrl): ?>
|
||
<a href="<?= e($privacyUrl) ?>" target="_blank" rel="noopener" style="color:#6b7280; font-size:.78rem"><?= e($privacyLabel) ?></a>
|
||
<?php else: ?>
|
||
<a href="#" onclick="openLegalPopup(<?= htmlspecialchars(json_encode($privacyHtml), ENT_QUOTES) ?>); return false;" style="color:#6b7280; font-size:.78rem"><?= e($privacyLabel) ?></a>
|
||
<?php endif; ?>
|
||
<?php endif; ?>
|
||
|
||
<?php if ($hasImprint): ?>
|
||
<?php if ($imprintUrl): ?>
|
||
<a href="<?= e($imprintUrl) ?>" target="_blank" rel="noopener" style="color:#6b7280; font-size:.78rem"><?= e($imprintLabel) ?></a>
|
||
<?php else: ?>
|
||
<a href="#" onclick="openLegalPopup(<?= htmlspecialchars(json_encode($imprintHtml), ENT_QUOTES) ?>); return false;" style="color:#6b7280; font-size:.78rem"><?= e($imprintLabel) ?></a>
|
||
<?php endif; ?>
|
||
<?php endif; ?>
|
||
</nav>
|
||
Copyright © <?= date('Y') ?> <?= e(get_setting('display_name', config()['site']['name'])) ?> – Alle Rechte vorbehalten.
|
||
</footer>
|
||
|
||
<?php
|
||
// Digitaler Assistent: nur wenn der Live-Chat ausgeschaltet oder gerade niemand erreichbar ist; dann ersetzt er das Live-Chat-Fenster
|
||
require_once __DIR__ . '/assistant.php';
|
||
$__assistantOn = false;
|
||
try { $__assistantOn = assistant_active(get_db()); } catch (Throwable $e) { /* Datenbank noch nicht aktualisiert: kein Assistent */ }
|
||
if ($__assistantOn) require __DIR__ . '/assistant_widget.php';
|
||
?>
|
||
<?php if (get_setting('live_chat_enabled', '0') === '1' && !$__assistantOn): ?>
|
||
<!-- Live-Chat-Widget: kann jederzeit unter Einstellungen → Allgemein wieder deaktiviert werden -->
|
||
<style>
|
||
@keyframes lc-pop { 0%{transform:scale(.8);opacity:0} 100%{transform:scale(1);opacity:1} }
|
||
@keyframes lc-pulse { 0%,100%{transform:scale(1)} 50%{transform:scale(1.08)} }
|
||
#lc-bubble.lc-unread{animation:lc-pulse 1.4s ease-in-out infinite}
|
||
.lc-msg-row{animation:lc-pop .18s ease-out}
|
||
</style>
|
||
<div id="lc-bubble" style="position:fixed;bottom:20px;right:20px;width:56px;height:56px;border-radius:50%;background:var(--primary);color:#fff;display:flex;align-items:center;justify-content:center;font-size:1.5rem;cursor:pointer;box-shadow:0 4px 16px rgba(0,0,0,.25);z-index:9998;user-select:none">
|
||
💬
|
||
<span id="lc-badge" style="display:none;position:absolute;top:-3px;right:-3px;background:#dc2626;color:#fff;border-radius:10px;min-width:20px;height:20px;font-size:.68rem;font-weight:700;align-items:center;justify-content:center;padding:0 5px;border:2px solid #fff"></span>
|
||
</div>
|
||
|
||
<div id="lc-panel" style="display:none;position:fixed;bottom:88px;right:20px;width:340px;max-width:calc(100vw - 32px);max-height:min(560px, calc(100vh - 120px));background:#fff;border-radius:12px;box-shadow:0 8px 32px rgba(0,0,0,.28);z-index:9998;overflow:hidden;flex-direction:column">
|
||
<div style="background:var(--primary);color:#fff;padding:14px 16px;flex-shrink:0">
|
||
<strong style="font-size:.92rem">💬 Live-Chat</strong>
|
||
<span id="lc-close" style="cursor:pointer;font-size:1.2rem;line-height:1;opacity:.85;float:right">×</span>
|
||
<div style="font-size:.72rem;opacity:.9;margin-top:3px;display:flex;align-items:center;gap:5px">
|
||
<span id="lc-status-dot" style="width:7px;height:7px;border-radius:50%;background:#9ca3af;display:inline-block;flex-shrink:0"></span>
|
||
<span id="lc-status-text">Verfügbarkeit wird geprüft…</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="lc-messages" style="flex:1;overflow-y:auto;padding:12px 14px;display:none;background:#f7f8fa"></div>
|
||
<div id="lc-typing" style="display:none;padding:0 14px 8px;font-size:.76rem;color:#9ca3af;background:#f7f8fa;font-style:italic">Mitarbeiter tippt …</div>
|
||
<div id="lc-ended" style="display:none;padding:16px;font-size:.85rem;color:#6b7280;text-align:center">
|
||
Dieser Chat wurde beendet.<br><a href="#" id="lc-restart" style="color:var(--primary);font-weight:600">Neuen Chat starten</a>
|
||
</div>
|
||
<div id="lc-converted" style="display:none;padding:20px 16px;font-size:.85rem;color:#374151;text-align:center">
|
||
<div style="font-size:1.6rem;margin-bottom:8px">✅</div>
|
||
<div style="font-weight:600;margin-bottom:4px">Vielen Dank! Ihr Ticket wurde erstellt.</div>
|
||
<div id="lc-converted-number" style="color:#6b7280;margin-bottom:8px"></div>
|
||
<div style="color:#6b7280">Da gerade niemand direkt verfügbar ist, antworten wir Ihnen per E-Mail, sobald wir Ihre Anfrage bearbeitet haben.</div>
|
||
</div>
|
||
|
||
<form id="lc-start-form" style="padding:14px 16px;display:flex;flex-direction:column;gap:8px">
|
||
<p style="margin:0 0 2px;font-size:.83rem;color:#374151">👋 Hallo! Wie können wir Ihnen helfen?</p>
|
||
<div id="lc-unavailable-hint" style="display:none;background:#fffbeb;border:1px solid #fde68a;color:#92400e;font-size:.78rem;padding:8px 10px;border-radius:6px">Gerade ist niemand verfügbar – wir antworten aber, sobald wir wieder da sind.</div>
|
||
<div id="lc-error" style="display:none;color:#dc2626;font-size:.78rem"></div>
|
||
<input type="text" id="lc-name" placeholder="Ihr Name" required style="padding:8px 10px;border:1px solid #d1d5db;border-radius:6px;font-size:.85rem">
|
||
<input type="email" id="lc-email" placeholder="Ihre E-Mail-Adresse" required style="padding:8px 10px;border:1px solid #d1d5db;border-radius:6px;font-size:.85rem">
|
||
<div style="position:absolute;left:-9999px" aria-hidden="true"><input type="text" id="lc-website" tabindex="-1" autocomplete="off"></div>
|
||
<textarea id="lc-message" placeholder="Ihre Nachricht…" required rows="3" style="padding:8px 10px;border:1px solid #d1d5db;border-radius:6px;font-size:.85rem;resize:vertical"></textarea>
|
||
<div style="display:flex;align-items:center;gap:8px">
|
||
<label for="lc-start-attachment" style="cursor:pointer;font-size:.78rem;color:#374151;display:flex;align-items:center;gap:4px;padding:5px 9px;border:1px solid #d1d5db;border-radius:6px">📎 Datei</label>
|
||
<input type="file" id="lc-start-attachment" style="position:absolute;left:-9999px" accept="image/*,.pdf,.doc,.docx,.txt">
|
||
<span id="lc-start-attachment-name" style="font-size:.76rem;color:#6b7280"></span>
|
||
</div>
|
||
<button type="submit" class="btn" id="lc-start-btn" style="justify-content:center">Chat starten</button>
|
||
</form>
|
||
|
||
<form id="lc-send-form" style="display:none;padding:10px 12px;gap:8px;border-top:1px solid #eee;flex-shrink:0;flex-direction:column">
|
||
<div id="lc-send-attachment-name" style="display:none;font-size:.76rem;color:#6b7280;padding:0 2px"></div>
|
||
<div style="display:flex;gap:8px">
|
||
<label for="lc-send-attachment" style="cursor:pointer;flex-shrink:0;display:flex;align-items:center;justify-content:center;width:36px;border:1px solid #d1d5db;border-radius:6px;font-size:1rem">📎</label>
|
||
<input type="file" id="lc-send-attachment" style="position:absolute;left:-9999px" accept="image/*,.pdf,.doc,.docx,.txt">
|
||
<input type="text" id="lc-send-input" placeholder="Nachricht…" style="flex:1;padding:9px 10px;border:1px solid #d1d5db;border-radius:6px;font-size:.85rem">
|
||
<button type="submit" class="btn" id="lc-send-btn" style="padding:8px 14px">Senden</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<script>
|
||
(function () {
|
||
var CSRF = <?= json_encode(csrf_token()) ?>;
|
||
var bubble = document.getElementById('lc-bubble');
|
||
var badge = document.getElementById('lc-badge');
|
||
var panel = document.getElementById('lc-panel');
|
||
var closeBtn = document.getElementById('lc-close');
|
||
var messagesEl = document.getElementById('lc-messages');
|
||
var typingEl = document.getElementById('lc-typing');
|
||
var endedEl = document.getElementById('lc-ended');
|
||
var convertedEl = document.getElementById('lc-converted');
|
||
var convertedNumberEl = document.getElementById('lc-converted-number');
|
||
var startForm = document.getElementById('lc-start-form');
|
||
var sendForm = document.getElementById('lc-send-form');
|
||
var sendInput = document.getElementById('lc-send-input');
|
||
var errorEl = document.getElementById('lc-error');
|
||
var statusDot = document.getElementById('lc-status-dot');
|
||
var statusText = document.getElementById('lc-status-text');
|
||
var pollTimer = null;
|
||
var renderedIds = {};
|
||
var opened = false;
|
||
var unreadCount = 0;
|
||
var lastTypingPingAt = 0;
|
||
var caughtUp = false; // wird nach dem ersten Laden true – erst danach lösen neue Nachrichten Ton/Badge aus
|
||
|
||
function playNotifySound() {
|
||
try {
|
||
var ctx = new (window.AudioContext || window.webkitAudioContext)();
|
||
var osc = ctx.createOscillator();
|
||
var gain = ctx.createGain();
|
||
osc.connect(gain); gain.connect(ctx.destination);
|
||
osc.frequency.value = 660;
|
||
gain.gain.setValueAtTime(0.12, ctx.currentTime);
|
||
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.3);
|
||
osc.start(); osc.stop(ctx.currentTime + 0.3);
|
||
} catch (e) {}
|
||
}
|
||
|
||
function showError(msg) {
|
||
errorEl.textContent = msg;
|
||
errorEl.style.display = 'block';
|
||
}
|
||
|
||
function setUnread(n) {
|
||
unreadCount = n;
|
||
if (n > 0 && !opened) {
|
||
badge.textContent = n > 9 ? '9+' : String(n);
|
||
badge.style.display = 'flex';
|
||
bubble.classList.add('lc-unread');
|
||
} else {
|
||
badge.style.display = 'none';
|
||
bubble.classList.remove('lc-unread');
|
||
}
|
||
}
|
||
|
||
function updateStatus(available) {
|
||
if (available === false) {
|
||
statusDot.style.background = '#9ca3af';
|
||
statusText.textContent = 'Gerade nicht erreichbar';
|
||
} else {
|
||
statusDot.style.background = '#22c55e';
|
||
statusText.textContent = 'Wir sind für Sie da';
|
||
}
|
||
}
|
||
|
||
function setStaffTyping(isTyping) {
|
||
typingEl.style.display = isTyping ? 'block' : 'none';
|
||
}
|
||
|
||
// Signalisiert dem Mitarbeiter "Kunde tippt gerade" – höchstens alle 3s, damit nicht
|
||
// bei jedem einzelnen Tastenanschlag eine Anfrage rausgeht. Fehler werden bewusst
|
||
// ignoriert (verpasstes Signal ist folgenlos, siehe live_chat.php Aktion "typing").
|
||
function pingTyping() {
|
||
var now = Date.now();
|
||
if (now - lastTypingPingAt < 3000) return;
|
||
lastTypingPingAt = now;
|
||
var body = new URLSearchParams();
|
||
body.set('action', 'typing');
|
||
body.set('csrf_token', CSRF);
|
||
fetch('/live_chat.php', { method: 'POST', body: body }).catch(function () {});
|
||
}
|
||
if (sendInput) sendInput.addEventListener('input', pingTyping);
|
||
|
||
/** Hängt nur wirklich neue Nachrichten an (statt alles neu zu rendern) – dadurch
|
||
* bleibt die Scroll-Position stabil und neue Blasen können sanft einblenden. */
|
||
function appendNewMessages(messages) {
|
||
var appended = 0;
|
||
messages.forEach(function (m) {
|
||
if (renderedIds[m.id]) return;
|
||
renderedIds[m.id] = true;
|
||
appended++;
|
||
|
||
var row = document.createElement('div');
|
||
row.className = 'lc-msg-row';
|
||
row.style.marginBottom = '4px';
|
||
row.style.display = 'flex';
|
||
row.style.flexDirection = 'column';
|
||
row.style.alignItems = m.authorType === 'customer' ? 'flex-end' : 'flex-start';
|
||
|
||
if (m.authorType === 'staff' && m.authorName) {
|
||
var nameEl = document.createElement('div');
|
||
nameEl.style.fontSize = '.68rem';
|
||
nameEl.style.color = '#9ca3af';
|
||
nameEl.style.margin = '6px 2px 2px';
|
||
nameEl.textContent = m.authorName;
|
||
row.appendChild(nameEl);
|
||
}
|
||
|
||
if (m.message) {
|
||
var bubbleEl = document.createElement('div');
|
||
bubbleEl.style.maxWidth = '78%';
|
||
bubbleEl.style.padding = '8px 11px';
|
||
bubbleEl.style.borderRadius = '10px';
|
||
bubbleEl.style.fontSize = '.85rem';
|
||
bubbleEl.style.whiteSpace = 'pre-wrap';
|
||
bubbleEl.style.wordBreak = 'break-word';
|
||
bubbleEl.style.marginBottom = '2px';
|
||
if (m.authorType === 'customer') {
|
||
bubbleEl.style.background = 'var(--primary)';
|
||
bubbleEl.style.color = '#fff';
|
||
} else {
|
||
bubbleEl.style.background = '#fff';
|
||
bubbleEl.style.border = '1px solid #e5e7eb';
|
||
bubbleEl.style.color = '#1f2937';
|
||
}
|
||
bubbleEl.textContent = m.message; // textContent = automatisch XSS-sicher, kein HTML-Parsing
|
||
row.appendChild(bubbleEl);
|
||
}
|
||
|
||
var imgExts = ['jpg','jpeg','png','gif','webp'];
|
||
(m.attachments || []).forEach(function (a) {
|
||
var ext = (a.name.split('.').pop() || '').toLowerCase();
|
||
if (imgExts.indexOf(ext) !== -1) {
|
||
var link = document.createElement('a');
|
||
link.href = '/download.php?id=' + a.id + '&preview=1';
|
||
link.target = '_blank';
|
||
link.style.cssText = 'display:block;margin-bottom:4px;border-radius:8px;overflow:hidden;max-width:180px';
|
||
var img = document.createElement('img');
|
||
img.src = '/download.php?id=' + a.id + '&preview=1';
|
||
img.alt = a.name;
|
||
img.style.cssText = 'display:block;max-width:100%;max-height:160px;object-fit:cover';
|
||
link.appendChild(img);
|
||
row.appendChild(link);
|
||
} else {
|
||
var fileLink = document.createElement('a');
|
||
fileLink.href = '/download.php?id=' + a.id;
|
||
fileLink.target = '_blank';
|
||
fileLink.style.cssText = 'display:inline-flex;align-items:center;gap:5px;font-size:.78rem;padding:5px 10px;background:#fff;border:1px solid #e5e7eb;border-radius:8px;text-decoration:none;color:#1f2937;margin-bottom:4px';
|
||
fileLink.textContent = '📎 ' + a.name;
|
||
row.appendChild(fileLink);
|
||
}
|
||
});
|
||
|
||
var timeEl = document.createElement('div');
|
||
timeEl.style.fontSize = '.65rem';
|
||
timeEl.style.color = '#9ca3af';
|
||
timeEl.textContent = (m.createdAt || '').substring(11, 16);
|
||
row.appendChild(timeEl);
|
||
|
||
messagesEl.appendChild(row);
|
||
|
||
// Beim allerersten Laden (Historie "nachladen") nie Ton/Badge auslösen – nur für
|
||
// Nachrichten, die tatsächlich NACH diesem Zeitpunkt neu reinkommen.
|
||
if (caughtUp && m.authorType === 'staff') {
|
||
if (!opened) setUnread(unreadCount + 1);
|
||
playNotifySound();
|
||
}
|
||
});
|
||
if (appended) messagesEl.scrollTop = messagesEl.scrollHeight;
|
||
caughtUp = true;
|
||
}
|
||
|
||
function showActiveChat(data) {
|
||
// Wurde der Chat (z.B. im Backend) inzwischen beendet, direkt den "Beendet"-Zustand
|
||
// zeigen, statt ein scheinbar aktives Eingabefeld für ein bereits geschlossenes Ticket.
|
||
// "Beendet" hat Vorrang vor "Ticket erstellt" – ein abgeschlossenes Ticket ist der
|
||
// endgültigere Zustand, egal ob es vorher ein normaler Chat war oder nicht.
|
||
if (data.status === 'closed') {
|
||
showEnded();
|
||
return;
|
||
}
|
||
// War gerade niemand verfügbar, wurde aus dem Chat ein ganz normales Ticket (kein
|
||
// Live-Chat mehr) – statt der eigenen Nachricht als Chat-Bubble zeigen wir dem Kunden
|
||
// eine klare Bestätigung, dass sein Anliegen als Ticket angekommen ist. Live-Polling
|
||
// lohnt hier nicht: das Ticket wird nicht nachträglich wieder zum Chat.
|
||
if (data.convertedToTicket) {
|
||
showConverted(data);
|
||
return;
|
||
}
|
||
startForm.style.display = 'none';
|
||
endedEl.style.display = 'none';
|
||
convertedEl.style.display = 'none';
|
||
messagesEl.style.display = 'block';
|
||
sendForm.style.display = 'flex';
|
||
updateStatus(data.staffAvailable);
|
||
setStaffTyping(!!data.staffTyping);
|
||
appendNewMessages(data.messages || []);
|
||
startPolling();
|
||
}
|
||
|
||
function showConverted(data) {
|
||
startForm.style.display = 'none';
|
||
messagesEl.style.display = 'none';
|
||
sendForm.style.display = 'none';
|
||
endedEl.style.display = 'none';
|
||
convertedEl.style.display = 'block';
|
||
convertedNumberEl.textContent = data.number ? ('Ticket #' + data.number) : '';
|
||
setStaffTyping(false);
|
||
stopPolling();
|
||
}
|
||
|
||
function showEnded() {
|
||
startForm.style.display = 'none';
|
||
messagesEl.style.display = 'none';
|
||
sendForm.style.display = 'none';
|
||
convertedEl.style.display = 'none';
|
||
endedEl.style.display = 'block';
|
||
setStaffTyping(false);
|
||
stopPolling();
|
||
}
|
||
|
||
function showStartForm(staffAvailable) {
|
||
startForm.style.display = 'flex';
|
||
messagesEl.style.display = 'none';
|
||
sendForm.style.display = 'none';
|
||
endedEl.style.display = 'none';
|
||
convertedEl.style.display = 'none';
|
||
updateStatus(staffAvailable);
|
||
setStaffTyping(false);
|
||
document.getElementById('lc-unavailable-hint').style.display = (staffAvailable === false) ? 'block' : 'none';
|
||
stopPolling();
|
||
}
|
||
|
||
function updateCsrf(data) {
|
||
if (data && data.csrfToken) CSRF = data.csrfToken;
|
||
}
|
||
|
||
function checkState() {
|
||
fetch('/live_chat.php?action=state')
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (data) {
|
||
updateCsrf(data);
|
||
// Wurde der vorherige Chat bereits (z.B. beim letzten Besuch, ohne dass der Kunde
|
||
// live dabei war) beendet ODER zu einem normalen Ticket, nicht extra die "Beendet"-
|
||
// bzw. "Ticket erstellt"-Meldung zeigen – der Kunde müsste sonst erst auf "Neuen
|
||
// Chat starten" klicken. Stattdessen beim erneuten Laden der Seite (bzw. erneuten
|
||
// Öffnen des Bubbles) direkt wieder das normale Startformular anzeigen, damit er bei
|
||
// Bedarf sofort einen neuen Chat/eine neue Anfrage starten kann. Passiert das
|
||
// Beenden/Umwandeln dagegen WÄHREND der Kunde live zusieht (Panel offen), bleibt die
|
||
// jeweilige Meldung bestehen (siehe startPolling() bzw. showActiveChat()) – da ist
|
||
// sie sinnvolles Feedback.
|
||
if (data.active && (data.status === 'closed' || data.convertedToTicket)) {
|
||
renderedIds = {};
|
||
messagesEl.innerHTML = '';
|
||
showStartForm(data.staffAvailable);
|
||
return;
|
||
}
|
||
if (data.active) showActiveChat(data);
|
||
else showStartForm(data.staffAvailable);
|
||
})
|
||
.catch(function () { showStartForm(); });
|
||
}
|
||
|
||
// Hält die Verfügbarkeitsanzeige ("Wir sind für Sie da" / "Gerade nicht erreichbar") live,
|
||
// unabhängig davon, ob gerade ein Chat läuft oder nur das Startformular gezeigt wird – ändert
|
||
// ein Mitarbeiter seinen Status, wird das hier binnen weniger Sekunden sichtbar, ohne dass der
|
||
// Kunde die Seite neu laden muss. Rührt bewusst nur Status/Hinweisbanner an, nicht das Formular.
|
||
function pollAvailability() {
|
||
fetch('/live_chat.php?action=state')
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (data) {
|
||
updateCsrf(data);
|
||
updateStatus(data.staffAvailable);
|
||
var hint = document.getElementById('lc-unavailable-hint');
|
||
if (hint && startForm.style.display !== 'none') {
|
||
hint.style.display = (data.staffAvailable === false) ? 'block' : 'none';
|
||
}
|
||
})
|
||
.catch(function () {});
|
||
}
|
||
setInterval(pollAvailability, 15000);
|
||
|
||
// Läuft dauerhaft im Hintergrund weiter (auch bei eingeklapptem Widget), damit Antworten
|
||
// per Ton + Zähler-Badge am Bubble-Icon auffallen, statt erst beim nächsten Öffnen bemerkt zu werden.
|
||
function startPolling() {
|
||
stopPolling();
|
||
pollTimer = setInterval(function () {
|
||
fetch('/live_chat.php?action=poll')
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (data) {
|
||
updateCsrf(data);
|
||
if (!data.active) { stopPolling(); return; }
|
||
if (data.status === 'closed') { if (opened) showEnded(); stopPolling(); return; }
|
||
if (opened) { updateStatus(data.staffAvailable); setStaffTyping(!!data.staffTyping); }
|
||
appendNewMessages(data.messages || []);
|
||
})
|
||
.catch(function () {});
|
||
}, 8000);
|
||
}
|
||
|
||
function stopPolling() {
|
||
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
|
||
}
|
||
|
||
function setBusy(btn, busy, busyLabel, idleLabel) {
|
||
btn.disabled = busy;
|
||
btn.textContent = busy ? busyLabel : idleLabel;
|
||
}
|
||
|
||
bubble.addEventListener('click', function () {
|
||
opened = panel.style.display !== 'flex';
|
||
panel.style.display = opened ? 'flex' : 'none';
|
||
if (opened) {
|
||
setUnread(0);
|
||
checkState();
|
||
}
|
||
});
|
||
closeBtn.addEventListener('click', function () {
|
||
opened = false;
|
||
panel.style.display = 'none';
|
||
});
|
||
|
||
// Dateiauswahl anzeigen/zurücksetzen – gemeinsam für Start- und Send-Formular.
|
||
function wireAttachmentPreview(inputEl, nameEl, isSpan) {
|
||
inputEl.addEventListener('change', function () {
|
||
var file = inputEl.files[0];
|
||
if (!file) { nameEl.textContent = ''; if (!isSpan) nameEl.style.display = 'none'; return; }
|
||
if (file.size > 15 * 1024 * 1024) {
|
||
showError('Die Datei ist zu groß (max. 15 MB).');
|
||
inputEl.value = '';
|
||
nameEl.textContent = '';
|
||
if (!isSpan) nameEl.style.display = 'none';
|
||
return;
|
||
}
|
||
nameEl.textContent = '📎 ' + file.name;
|
||
if (!isSpan) nameEl.style.display = 'block';
|
||
});
|
||
}
|
||
var startAttachment = document.getElementById('lc-start-attachment');
|
||
var startAttachmentName = document.getElementById('lc-start-attachment-name');
|
||
var sendAttachment = document.getElementById('lc-send-attachment');
|
||
var sendAttachmentName = document.getElementById('lc-send-attachment-name');
|
||
wireAttachmentPreview(startAttachment, startAttachmentName, true);
|
||
wireAttachmentPreview(sendAttachment, sendAttachmentName, false);
|
||
|
||
startForm.addEventListener('submit', function (e) {
|
||
e.preventDefault();
|
||
errorEl.style.display = 'none';
|
||
var startBtn = document.getElementById('lc-start-btn');
|
||
var body = new FormData();
|
||
body.set('action', 'start');
|
||
body.set('csrf_token', CSRF);
|
||
body.set('name', document.getElementById('lc-name').value);
|
||
body.set('email', document.getElementById('lc-email').value);
|
||
body.set('message', document.getElementById('lc-message').value);
|
||
body.set('website', document.getElementById('lc-website').value);
|
||
if (startAttachment.files[0]) body.set('attachment', startAttachment.files[0]);
|
||
setBusy(startBtn, true, 'Wird gestartet…', 'Chat starten');
|
||
fetch('/live_chat.php', { method: 'POST', body: body })
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (data) {
|
||
updateCsrf(data);
|
||
if (data.error) { showError(data.error); return; }
|
||
document.getElementById('lc-message').value = '';
|
||
startAttachment.value = '';
|
||
startAttachmentName.textContent = '';
|
||
showActiveChat(data);
|
||
})
|
||
.catch(function () { showError('Verbindung fehlgeschlagen. Bitte versuchen Sie es erneut.'); })
|
||
.finally(function () { setBusy(startBtn, false, 'Wird gestartet…', 'Chat starten'); });
|
||
});
|
||
|
||
sendForm.addEventListener('submit', function (e) {
|
||
e.preventDefault();
|
||
var input = document.getElementById('lc-send-input');
|
||
var sendBtn = document.getElementById('lc-send-btn');
|
||
var text = input.value.trim();
|
||
var file = sendAttachment.files[0];
|
||
if (!text && !file) return;
|
||
var body = new FormData();
|
||
body.set('action', 'send');
|
||
body.set('csrf_token', CSRF);
|
||
body.set('message', text);
|
||
if (file) body.set('attachment', file);
|
||
input.value = '';
|
||
setBusy(sendBtn, true, '…', 'Senden');
|
||
fetch('/live_chat.php', { method: 'POST', body: body })
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (data) {
|
||
updateCsrf(data);
|
||
if (data.ended) { showEnded(); return; }
|
||
if (data.error) { return; }
|
||
sendAttachment.value = '';
|
||
sendAttachmentName.textContent = '';
|
||
sendAttachmentName.style.display = 'none';
|
||
appendNewMessages(data.messages || []);
|
||
})
|
||
.catch(function () {})
|
||
.finally(function () { setBusy(sendBtn, false, '…', 'Senden'); input.focus(); });
|
||
});
|
||
|
||
document.getElementById('lc-restart').addEventListener('click', function (e) {
|
||
e.preventDefault();
|
||
renderedIds = {};
|
||
messagesEl.innerHTML = '';
|
||
showStartForm();
|
||
});
|
||
|
||
// Beim Laden der Seite bereits prüfen, ob ein Chat weiterläuft, damit Antworten auch
|
||
// erkannt werden (Ton + Badge), ohne dass der Kunde das Widget vorher öffnen muss.
|
||
checkState();
|
||
})();
|
||
</script>
|
||
<?php endif; ?>
|