Files
deskhub/DeskHub-Webseite/staff/chat.php
T
2026-10-16 19:12:21 +00:00

325 lines
15 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
require_once __DIR__ . '/../includes/bootstrap.php';
$staff = require_staff_login();
$pdo = get_db();
$withId = (int) ($_GET['with'] ?? 0);
// Alle Mitarbeiter für Kontaktliste
$allStaff = $pdo->query('SELECT id, name, is_active FROM staff ORDER BY name')->fetchAll();
// Ungelesene Nachrichten je Absender
$unreadStmt = $pdo->prepare(
'SELECT from_staff_id, COUNT(*) AS cnt FROM staff_chat_messages WHERE to_staff_id=? AND is_read=0 GROUP BY from_staff_id'
);
$unreadStmt->execute([$staff['id']]);
$unreadByStaff = [];
foreach ($unreadStmt->fetchAll() as $r) {
$unreadByStaff[$r['from_staff_id']] = (int)$r['cnt'];
}
$conversation = [];
$withStaff = null;
if ($withId) {
$s = $pdo->prepare('SELECT * FROM staff WHERE id=?');
$s->execute([$withId]);
$withStaff = $s->fetch();
// Als gelesen markieren
$pdo->prepare('UPDATE staff_chat_messages SET is_read=1 WHERE from_staff_id=? AND to_staff_id=?')
->execute([$withId, $staff['id']]);
// Konversation laden
$cStmt = $pdo->prepare(
"SELECT m.*, sf.name AS from_name
FROM staff_chat_messages m
JOIN staff sf ON sf.id = m.from_staff_id
WHERE (m.from_staff_id=? AND m.to_staff_id=?)
OR (m.from_staff_id=? AND m.to_staff_id=?)
ORDER BY m.created_at ASC LIMIT 200"
);
$cStmt->execute([$staff['id'], $withId, $withId, $staff['id']]);
$conversation = $cStmt->fetchAll();
}
// Nachricht senden (AJAX oder normal)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf();
$toId = (int) ($_POST['to_id'] ?? 0);
$msg = trim($_POST['message'] ?? '');
if ($toId && $msg !== '') {
$pdo->prepare('INSERT INTO staff_chat_messages (from_staff_id, to_staff_id, message, created_at) VALUES (?,?,?,?)')
->execute([$staff['id'], $toId, $msg, db_now()]);
}
if (!empty($_SERVER['HTTP_X_REQUESTED_WITH'])) {
header('Content-Type: application/json');
echo json_encode(['ok' => true]);
exit;
}
redirect('/staff/chat.php?with=' . $toId);
}
// AJAX: neue Nachrichten pollen
if (($_GET['action'] ?? '') === 'poll' && $withId) {
header('Content-Type: application/json');
$since = $_GET['since'] ?? '1970-01-01 00:00:00';
$pStmt = $pdo->prepare(
"SELECT m.*, sf.name AS from_name FROM staff_chat_messages m
JOIN staff sf ON sf.id = m.from_staff_id
WHERE ((m.from_staff_id=? AND m.to_staff_id=?) OR (m.from_staff_id=? AND m.to_staff_id=?))
AND m.created_at > ?
ORDER BY m.created_at ASC"
);
$pStmt->execute([$staff['id'], $withId, $withId, $staff['id'], $since]);
$pdo->prepare('UPDATE staff_chat_messages SET is_read=1 WHERE from_staff_id=? AND to_staff_id=?')
->execute([$withId, $staff['id']]);
echo json_encode($pStmt->fetchAll());
exit;
}
$pageTitle = 'Interner Chat';
$activePage = 'chat';
require __DIR__ . '/_header.php';
?>
<style>
*{box-sizing:border-box}
.ac-wrap{display:flex;height:calc(100vh - 56px);margin:-32px -28px -60px;overflow:hidden;background:#fff;border-top:1px solid #e5e7eb}
.ac-main{flex:1;display:flex;flex-direction:column;min-width:0;background:#fff}
.ac-topbar{padding:12px 18px;border-bottom:1px solid #e5e7eb;display:flex;align-items:center;gap:12px;background:#fff;flex-shrink:0}
.ac-topbar-av{width:38px;height:38px;border-radius:50%;background:var(--primary);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:.88rem;flex-shrink:0}
.ac-msgs{flex:1;overflow-y:auto;padding:16px 20px;display:flex;flex-direction:column;gap:0}
.ac-msg{display:flex;align-items:flex-start;gap:10px;padding:4px 0}
.ac-msg-av{width:34px;height:34px;border-radius:50%;color:#fff;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:.78rem;flex-shrink:0;margin-top:14px}
.ac-msg-body{display:flex;flex-direction:column;min-width:0}
.ac-msg-name{font-size:.7rem;color:#9ca3af;margin-bottom:2px}
.ac-msg-bubble{display:inline-block;padding:7px 12px;border-radius:18px;font-size:.88rem;line-height:1.45;word-break:break-word;max-width:480px}
.ac-msg.me{flex-direction:row-reverse}
.ac-msg.me .ac-msg-av{display:none}
.ac-msg.me .ac-msg-body{align-items:flex-end}
.ac-msg.me .ac-msg-bubble{background:var(--primary);color:#fff}
.ac-msg.them .ac-msg-bubble{background:#f3f4f6;color:#111827}
.ac-msg-time{font-size:.65rem;color:#d1d5db;margin-top:2px;padding:0 2px}
.ac-inp-wrap{border-top:1px solid #e5e7eb;padding:10px 14px;display:flex;align-items:center;gap:10px;background:#fff;flex-shrink:0}
.ac-inp{flex:1;border:none;background:transparent;font-size:.9rem;font-family:var(--font-body);color:#111827;outline:none;padding:4px 0}
.ac-inp::placeholder{color:#9ca3af}
.ac-inp-btn{width:34px;height:34px;border-radius:50%;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:1.1rem;transition:all .15s;background:none;color:#9ca3af}
.ac-inp-btn:hover{color:var(--primary)}
.ac-send{background:var(--primary);color:#fff;width:34px;height:34px;border-radius:50%;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background .15s}
.ac-send:hover{background:var(--primary-dk)}
.ac-side{width:220px;flex-shrink:0;border-left:1px solid #e5e7eb;display:flex;flex-direction:column;background:#fff}
.ac-side-title{padding:14px 16px;font-size:.72rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#9ca3af;border-bottom:1px solid #e5e7eb}
.ac-side-list{flex:1;overflow-y:auto;padding:8px 0}
.ac-user{display:flex;align-items:center;gap:10px;padding:8px 14px;text-decoration:none;border-radius:0;transition:background .12s;cursor:pointer}
.ac-user:hover{background:#f9fafb}
.ac-user.on{background:#f0fdf4}
.ac-user-av{width:36px;height:36px;border-radius:50%;color:#fff;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:.82rem;flex-shrink:0;position:relative}
.ac-user-av::after{content:'';position:absolute;bottom:1px;right:1px;width:9px;height:9px;border-radius:50%;background:#22c55e;border:2px solid #fff}
.ac-user-av.off::after{background:#d1d5db}
.ac-user-name{font-size:.84rem;font-weight:500;color:#111827;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ac-user-sub{font-size:.71rem;color:#9ca3af}
.ac-bdg{background:var(--danger);color:#fff;border-radius:10px;min-width:18px;height:18px;font-size:.65rem;font-weight:700;display:flex;align-items:center;justify-content:center;padding:0 4px;margin-left:auto}
.ac-empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#9ca3af;gap:10px}
.ep{position:relative}
.eg{position:absolute;bottom:calc(100% + 6px);left:50%;transform:translateX(-50%);background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:8px;display:none;grid-template-columns:repeat(8,1fr);gap:2px;box-shadow:0 4px 16px rgba(0,0,0,.1);z-index:100;width:272px}
.eg.open{display:grid}
.eb{font-size:1.2rem;padding:5px;border:none;background:none;cursor:pointer;border-radius:6px;line-height:1}
.eb:hover{background:#f3f4f6}
/* Avatar-Farben je Initiale */
.av-a{background:#6366f1}.av-b{background:#8b5cf6}.av-c{background:#ec4899}.av-d{background:#14b8a6}
.av-e{background:#f59e0b}.av-f{background:#10b981}.av-g{background:#3b82f6}.av-h{background:#ef4444}
.av-i{background:#6366f1}.av-j{background:#8b5cf6}.av-k{background:#ec4899}.av-l{background:#14b8a6}
.av-m{background:#f59e0b}.av-n{background:#10b981}.av-o{background:#3b82f6}.av-p{background:#ef4444}
.av-q{background:#6366f1}.av-r{background:#8b5cf6}.av-s{background:#ec4899}.av-t{background:var(--primary)}
.av-u{background:#f59e0b}.av-v{background:#10b981}.av-w{background:#3b82f6}.av-x{background:#ef4444}
.av-y{background:#6366f1}.av-z{background:#8b5cf6}
</style>
<?php
function chatAvClass(string $name): string {
$l = strtolower(mb_substr($name, 0, 1));
return 'av-' . (ctype_alpha($l) ? $l : 'a');
}
?>
<div class="ac-wrap">
<!-- Hauptbereich -->
<div class="ac-main">
<?php if (!$withStaff): ?>
<div class="ac-empty">
<svg width="52" height="52" viewBox="0 0 24 24" fill="none" stroke="#d1d5db" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
<div style="font-size:.9rem;font-weight:600;color:#374151">Mitarbeiter auswählen</div>
<div style="font-size:.8rem;color:#9ca3af">Klicke rechts auf einen Mitarbeiter</div>
</div>
<?php else:
$ini2 = mb_strtoupper(mb_substr($withStaff['name'], 0, 1));
$cls2 = chatAvClass($withStaff['name']);
?>
<!-- Topbar -->
<div class="ac-topbar">
<div class="ac-topbar-av <?= $cls2 ?>"><?= e($ini2) ?></div>
<div>
<div style="font-weight:700;font-size:.93rem;color:#111827;line-height:1"><?= e($withStaff['name']) ?></div>
<div style="font-size:.74rem;color:#9ca3af;margin-top:2px"><?= e($withStaff['email']) ?></div>
</div>
</div>
<!-- Nachrichten -->
<div id="chat-messages" class="ac-msgs">
<?php if (!$conversation): ?>
<div style="text-align:center;color:#9ca3af;font-size:.82rem;padding:30px 0;margin:auto">Noch keine Nachrichten – schreib etwas!</div>
<?php endif; ?>
<?php
$prevDate = '';
foreach ($conversation as $msg):
$isMe = $msg['from_staff_id'] == $staff['id'];
$ini = mb_strtoupper(mb_substr($msg['from_name'], 0, 1));
$cls = chatAvClass($msg['from_name']);
$date = substr($msg['created_at'], 0, 10);
if ($date !== $prevDate):
$prevDate = $date;
?>
<div style="text-align:center;font-size:.72rem;color:#9ca3af;padding:12px 0"><?= date('d.m.Y', strtotime($date)) ?></div>
<?php endif; ?>
<div class="ac-msg <?= $isMe?'me':'them' ?>" data-ts="<?= e($msg['created_at']) ?>">
<?php if (!$isMe): ?>
<div class="ac-msg-av <?= $cls ?>"><?= e($ini) ?></div>
<?php endif; ?>
<div class="ac-msg-body">
<?php if (!$isMe): ?><div class="ac-msg-name"><?= e($msg['from_name']) ?></div><?php endif; ?>
<div class="ac-msg-bubble"><?= nl2br(e($msg['message'])) ?></div>
<div class="ac-msg-time"><?= date('H:i', strtotime($msg['created_at'])) ?></div>
</div>
</div>
<?php endforeach; ?>
</div>
<!-- Eingabe -->
<div class="ac-inp-wrap">
<div class="ep">
<button type="button" class="ac-inp-btn" id="emoji-toggle" title="Emoji">&#128578;</button>
<div class="eg" id="emoji-grid">
<?php foreach(['😀','😂','😍','😎','🤔','😅','🙏','👍','👎','❤️','🔥','✅','❌','⚠️','🎉','💯','😊','😢','😡','🤣','😇','🥳','😴','🤝','💪','👏','🚀','💡','📌','✏️','📞','📧','🔒','🔑','⏰','📅'] as $em): ?>
<button type="button" class="eb" onclick="insertEmoji('<?= $em ?>')"><?= $em ?></button>
<?php endforeach; ?>
</div>
</div>
<form id="chat-form" style="display:contents">
<?= csrf_field() ?>
<input type="hidden" name="to_id" value="<?= $withId ?>">
<input type="text" id="chat-input" name="message" class="ac-inp" placeholder="Aa"
onkeydown="if(event.key==='Enter'){event.preventDefault();sendMsg();}">
<button type="button" class="ac-send" onclick="sendMsg()" title="Senden">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
</button>
</form>
</div>
<?php endif; ?>
</div>
<!-- Rechte Sidebar: Online-Mitarbeiter -->
<div class="ac-side">
<div class="ac-side-title">Online Mitarbeiter</div>
<div class="ac-side-list">
<?php foreach ($allStaff as $s): if ($s['id'] == $staff['id']) continue;
$unread = $unreadByStaff[$s['id']] ?? 0;
$ini = mb_strtoupper(mb_substr($s['name'], 0, 1));
$cls = chatAvClass($s['name']);
?>
<a href="/staff/chat.php?with=<?= $s['id'] ?>" class="ac-user <?= $withId==$s['id']?'on':'' ?>">
<div class="ac-user-av <?= $cls ?> <?= !$s['is_active']?'off':'' ?>"><?= e($ini) ?></div>
<div style="flex:1;min-width:0">
<div class="ac-user-name"><?= e($s['name']) ?></div>
<div class="ac-user-sub"><?= $s['is_active']?'Online':'Offline' ?></div>
</div>
<?php if ($unread): ?><div class="ac-bdg"><?= $unread>9?'9+':$unread ?></div><?php endif; ?>
</a>
<?php endforeach; ?>
</div>
</div>
</div>
<?php if ($withStaff): ?>
<script>
var myId = <?= $staff['id'] ?>;
var withId = <?= $withId ?>;
var csrfToken = document.querySelector('[name=csrf_token]').value;
// Token nach Rotation aktualisieren
function getCsrf() { return document.querySelector('[name=csrf_token]').value; }
var lastTs = <?= $conversation ? json_encode(end($conversation)['created_at']) : json_encode('1970-01-01 00:00:00') ?>;
function insertEmoji(em) {
var input = document.getElementById('chat-input');
var pos = input.selectionStart || input.value.length;
input.value = input.value.slice(0, pos) + em + input.value.slice(pos);
input.focus();
document.getElementById('emoji-grid').classList.remove('open');
}
document.getElementById('emoji-toggle')?.addEventListener('click', function(e) {
e.stopPropagation();
document.getElementById('emoji-grid').classList.toggle('open');
});
document.addEventListener('click', function() {
document.getElementById('emoji-grid')?.classList.remove('open');
});
function scrollBottom() {
var el = document.getElementById('chat-messages');
el.scrollTop = el.scrollHeight;
}
scrollBottom();
function appendBubble(msg) {
var isMe = msg.from_staff_id == myId;
var div = document.createElement('div');
div.className = 'chat-bubble ' + (isMe ? 'me' : 'them');
div.dataset.ts = msg.created_at;
div.innerHTML = '<div class="bubble-text">' + msg.message.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/\n/g,'<br>') + '</div>'
+ '<div class="bubble-time">' + msg.created_at + '</div>';
document.getElementById('chat-messages').appendChild(div);
lastTs = msg.created_at;
scrollBottom();
}
function sendMsg() {
var input = document.getElementById('chat-input');
var msg = input.value.trim();
if (!msg) return;
input.value = '';
input.focus();
fetch('/staff/chat.php', {
method: 'POST',
headers: {'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
body: 'csrf_token=' + encodeURIComponent(getCsrf()) + '&to_id=' + withId + '&message=' + encodeURIComponent(msg)
}).then(function() {
appendBubble({from_staff_id: myId, message: msg, created_at: new Date().toISOString().slice(0,19).replace('T',' ')});
poll();
});
}
function poll() {
fetch('/staff/chat.php?with=' + withId + '&action=poll&since=' + encodeURIComponent(lastTs))
.then(function(r){ return r.json(); })
.then(function(msgs) {
msgs.forEach(function(m) {
if (m.from_staff_id != myId) appendBubble(m);
lastTs = m.created_at;
});
});
}
setInterval(poll, 8000);
</script>
<?php endif; ?>
<?php require __DIR__ . '/_footer.php'; ?>