diff --git a/public/admin.css b/public/admin.css index 335094e..1bff282 100644 --- a/public/admin.css +++ b/public/admin.css @@ -168,6 +168,9 @@ label { display: grid; gap: 6px; color: var(--muted); font-size: 13px; } text-overflow: ellipsis; white-space: nowrap; } +.visitor-name-wrap .online-dot { + flex: 0 0 auto; +} .unread-dot { width: 9px; height: 9px; diff --git a/public/admin.js b/public/admin.js index f82a3b3..f358fb9 100644 --- a/public/admin.js +++ b/public/admin.js @@ -3,6 +3,7 @@ const state = { snippet: "", conversations: [], visitors: [], + visitorsLoaded: false, activeId: null, active: null, events: null, @@ -237,12 +238,16 @@ async function loadBootstrap() { async function loadConversations() { const data = await api("/api/admin/conversations"); state.conversations = data.conversations; + syncConversationVisitors(); renderConversations(); } async function loadVisitors() { const data = await api("/api/admin/visitors"); state.visitors = data.visitors; + state.visitorsLoaded = true; + syncConversationVisitors(); + renderConversations(); renderVisitors(); } @@ -253,6 +258,7 @@ async function openConversation(id) { state.active = data; const index = state.conversations.findIndex((item) => item.id === id); if (index >= 0) state.conversations[index] = { ...state.conversations[index], ...data.conversation }; + syncConversationVisitors(); renderConversations(); renderActive(); } @@ -271,6 +277,9 @@ function connectEvents() { } if (data.type === "visitors:update") { state.visitors = data.visitors; + state.visitorsLoaded = true; + syncConversationVisitors(); + renderConversations(); renderVisitors(); return; } @@ -311,7 +320,8 @@ function renderConversations() { - ${escapeHtml(item.visitor.name || item.visitor.email || "Navstevnik")} + ${escapeHtml(conversationVisitorName(item))} + ${item.visitorOnline ? `` : ""} ${item.imageCount ? `foto ${item.imageCount}` : ""} @@ -332,6 +342,19 @@ function renderConversations() { `).join("") : `

Zadny chat pro vybrany filtr.

`; } +function syncConversationVisitors() { + if (!state.visitorsLoaded) return; + const onlineIds = new Set(state.visitors.map((visitor) => visitor.id)); + state.conversations = state.conversations.map((item) => ({ + ...item, + visitorOnline: onlineIds.has(item.visitorToken) + })); +} + +function conversationVisitorName(item) { + return item.visitor?.name || item.visitor?.email || item.visitorLabel || "Navstevnik"; +} + function renderVisitors() { const visitors = state.hideBots ? state.visitors.filter((visitor) => !visitor.isBot) : state.visitors; const botCount = state.visitors.filter((visitor) => visitor.isBot).length; diff --git a/server.js b/server.js index 127c20b..1ab6353 100644 --- a/server.js +++ b/server.js @@ -304,6 +304,12 @@ function adminConversations(res) { AND unread.sender_type = 'visitor' AND datetime(unread.created_at) > datetime(COALESCE(c.admin_seen_at, '1970-01-01 00:00:00')) ) AS has_unread, + EXISTS( + SELECT 1 FROM visitor_sessions vs + WHERE vs.site_id = c.site_id + AND vs.visitor_token = c.visitor_token + AND datetime(vs.last_seen_at) >= datetime('now', '-90 seconds') + ) AS visitor_online, (SELECT COUNT(*) FROM conversations vc WHERE vc.site_id = c.site_id AND vc.visitor_token = c.visitor_token) AS visitor_conversation_count FROM conversations c JOIN sites s ON s.id = c.site_id @@ -689,6 +695,10 @@ function formatConversationSummary(row) { siteId: row.site_id, siteKey: row.site_key, status: row.status, + visitorToken: row.visitor_token, + visitorInitials: visitorInitials(row.visitor_token), + visitorLabel: visitorLabel(row.visitor_token), + visitorOnline: Boolean(row.visitor_online), visitor: { name: row.visitor_name, email: row.visitor_email, @@ -765,6 +775,12 @@ function findConversation(publicId) { AND unread.sender_type = 'visitor' AND datetime(unread.created_at) > datetime(COALESCE(c.admin_seen_at, '1970-01-01 00:00:00')) ) AS has_unread, + EXISTS( + SELECT 1 FROM visitor_sessions vs + WHERE vs.site_id = c.site_id + AND vs.visitor_token = c.visitor_token + AND datetime(vs.last_seen_at) >= datetime('now', '-90 seconds') + ) AS visitor_online, (SELECT COUNT(*) FROM conversations vc WHERE vc.site_id = c.site_id AND vc.visitor_token = c.visitor_token) AS visitor_conversation_count FROM conversations c JOIN sites s ON s.id = c.site_id