364 lines
17 KiB
PHP
364 lines
17 KiB
PHP
<?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);
|
||
}
|
||
|
||
function chatAvClass(string $name): string {
|
||
$l = strtolower(mb_substr($name, 0, 1));
|
||
return 'av-' . (ctype_alpha($l) ? $l : 'a');
|
||
}
|
||
|
||
// AJAX: neue Nachrichten pollen
|
||
if (($_GET['action'] ?? '') === 'poll' && $withId) {
|
||
header('Content-Type: application/json');
|
||
$after = max(0, (int) ($_GET['after'] ?? 0));
|
||
$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.id > ?
|
||
ORDER BY m.id ASC"
|
||
);
|
||
$pStmt->execute([$staff['id'], $withId, $withId, $staff['id'], $after]);
|
||
$pdo->prepare('UPDATE staff_chat_messages SET is_read=1 WHERE from_staff_id=? AND to_staff_id=?')
|
||
->execute([$withId, $staff['id']]);
|
||
// Anzeige-Angaben gleich mitliefern: Das Gerüst im Browser baut die Blase genau wie die Seite selbst
|
||
echo json_encode(array_map(fn(array $m) => [
|
||
'id' => (int) $m['id'], 'from_staff_id' => (int) $m['from_staff_id'], 'from_name' => $m['from_name'], 'message' => $m['message'],
|
||
'created_at' => $m['created_at'], 'initial' => mb_strtoupper(mb_substr($m['from_name'], 0, 1)), 'av_class' => chatAvClass($m['from_name']),
|
||
], $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
|
||
?>
|
||
|
||
<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 id="chat-empty" 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-id="<?= (int) $msg['id'] ?>" 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">🙂</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 lastId = <?= $conversation ? (int) end($conversation)['id'] : 0 ?>;
|
||
|
||
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 box = document.getElementById('chat-messages');
|
||
if (box.querySelector('[data-id="' + msg.id + '"]')) return; // schon da
|
||
var empty = document.getElementById('chat-empty');
|
||
if (empty) empty.remove();
|
||
var isMe = msg.from_staff_id == myId;
|
||
var all = box.querySelectorAll('.ac-msg');
|
||
var last = all.length ? all[all.length - 1] : null;
|
||
var day = msg.created_at.slice(0, 10);
|
||
if (!last || last.dataset.ts.slice(0, 10) !== day) {
|
||
var sep = document.createElement('div');
|
||
sep.style.cssText = 'text-align:center;font-size:.72rem;color:#9ca3af;padding:12px 0';
|
||
sep.textContent = day.slice(8, 10) + '.' + day.slice(5, 7) + '.' + day.slice(0, 4);
|
||
box.appendChild(sep);
|
||
}
|
||
var row = document.createElement('div');
|
||
row.className = 'ac-msg ' + (isMe ? 'me' : 'them');
|
||
row.dataset.id = msg.id;
|
||
row.dataset.ts = msg.created_at;
|
||
if (!isMe) {
|
||
var av = document.createElement('div');
|
||
av.className = 'ac-msg-av ' + msg.av_class;
|
||
av.textContent = msg.initial;
|
||
row.appendChild(av);
|
||
}
|
||
var body = document.createElement('div');
|
||
body.className = 'ac-msg-body';
|
||
if (!isMe) {
|
||
var name = document.createElement('div');
|
||
name.className = 'ac-msg-name';
|
||
name.textContent = msg.from_name;
|
||
body.appendChild(name);
|
||
}
|
||
var bubble = document.createElement('div');
|
||
bubble.className = 'ac-msg-bubble';
|
||
msg.message.split('\n').forEach(function (line, i) {
|
||
if (i) bubble.appendChild(document.createElement('br'));
|
||
bubble.appendChild(document.createTextNode(line));
|
||
});
|
||
body.appendChild(bubble);
|
||
var time = document.createElement('div');
|
||
time.className = 'ac-msg-time';
|
||
time.textContent = msg.created_at.slice(11, 16);
|
||
body.appendChild(time);
|
||
row.appendChild(body);
|
||
box.appendChild(row);
|
||
if (msg.id > lastId) lastId = msg.id;
|
||
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() { poll(); }); // die eigene Nachricht kommt mit Nummer und Serverzeit zurück
|
||
}
|
||
|
||
var polling = false;
|
||
function poll() {
|
||
if (polling) return;
|
||
polling = true;
|
||
fetch('/staff/chat.php?with=' + withId + '&action=poll&after=' + lastId)
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(msgs) { msgs.forEach(appendBubble); })
|
||
.catch(function() {})
|
||
.then(function() { polling = false; });
|
||
}
|
||
setInterval(poll, 8000);
|
||
</script>
|
||
<?php endif; ?>
|
||
|
||
<?php require __DIR__ . '/_footer.php'; ?>
|