Show operator name groups in widget

This commit is contained in:
rajchales-monito
2026-07-24 10:44:12 +02:00
parent e1d63db62f
commit 69fa9075ab
3 changed files with 22 additions and 6 deletions
+1
View File
@@ -11,6 +11,7 @@ AUTH_SECRET=change-me-generate-long-random-secret
# Admin users, extend with USER_2/PASS_2, USER_3/PASS_3, ... # Admin users, extend with USER_2/PASS_2, USER_3/PASS_3, ...
USER_1=admin USER_1=admin
PASS_1=change-me PASS_1=change-me
NAME_1=Alex
# Seed first company/site # Seed first company/site
DEFAULT_COMPANY_NAME=MAAL DEFAULT_COMPANY_NAME=MAAL
+12 -2
View File
@@ -252,13 +252,22 @@
function renderConversation(messages, data) { function renderConversation(messages, data) {
messages.innerHTML = ""; messages.innerHTML = "";
let lastAdminName = null;
for (const message of data.messages) { for (const message of data.messages) {
addMessage(messages, message.senderType, message.body, message.attachments || []); addMessage(messages, message.senderType, message.senderName, message.body, message.attachments || [], lastAdminName);
if (message.senderType === "admin") lastAdminName = message.senderName || "Operator";
else lastAdminName = null;
} }
messages.scrollTop = messages.scrollHeight; messages.scrollTop = messages.scrollHeight;
} }
function addMessage(messages, sender, body, attachments) { function addMessage(messages, sender, senderName, body, attachments, lastAdminName) {
if (sender === "admin" && (senderName || "Operator") !== lastAdminName) {
const label = document.createElement("div");
label.className = "mf-operator-label";
label.textContent = `${senderName || "Operator"} odpovida`;
messages.append(label);
}
const item = document.createElement("article"); const item = document.createElement("article");
item.className = `mf-message ${sender}`; item.className = `mf-message ${sender}`;
item.innerHTML = `${body ? `<p>${escapeHtml(body)}</p>` : ""}${attachments.map((a) => `<a class="mf-photo" href="${apiBase}${a.url}" target="_blank" rel="noreferrer"><img src="${apiBase}${a.url}" alt="${escapeHtml(a.name)}"></a>`).join("")}`; item.innerHTML = `${body ? `<p>${escapeHtml(body)}</p>` : ""}${attachments.map((a) => `<a class="mf-photo" href="${apiBase}${a.url}" target="_blank" rel="noreferrer"><img src="${apiBase}${a.url}" alt="${escapeHtml(a.name)}"></a>`).join("")}`;
@@ -367,6 +376,7 @@
.mf-message { max-width: 86%; padding: 9px 11px; border-radius: 8px; background: #f1f5f2; color: ${c.text}; } .mf-message { max-width: 86%; padding: 9px 11px; border-radius: 8px; background: #f1f5f2; color: ${c.text}; }
.mf-message.admin { align-self: flex-start; background: #eef7f4; } .mf-message.admin { align-self: flex-start; background: #eef7f4; }
.mf-message.visitor { align-self: flex-end; background: ${c.brand}; color: ${c.brandText}; } .mf-message.visitor { align-self: flex-end; background: ${c.brand}; color: ${c.brandText}; }
.mf-operator-label { align-self: flex-start; margin: 4px 0 -4px; color: ${c.muted}; font-size: 12px; font-weight: 700; }
.mf-message p { margin: 0; white-space: pre-wrap; } .mf-message p { margin: 0; white-space: pre-wrap; }
.mf-message a { color: inherit; display: inline-block; margin-top: 6px; } .mf-message a { color: inherit; display: inline-block; margin-top: 6px; }
.mf-photo img { display: block; width: min(180px, 100%); max-height: 130px; object-fit: cover; border-radius: 6px; } .mf-photo img { display: block; width: min(180px, 100%); max-height: 130px; object-fit: cover; border-radius: 6px; }
+9 -4
View File
@@ -229,19 +229,24 @@ function configuredUsers() {
for (let i = 1; i < 50; i += 1) { for (let i = 1; i < 50; i += 1) {
const username = process.env[`USER_${i}`]; const username = process.env[`USER_${i}`];
const password = process.env[`PASS_${i}`]; const password = process.env[`PASS_${i}`];
const displayName = process.env[`NAME_${i}`] || username;
if (!username && !password) break; if (!username && !password) break;
if (username && password) users.push({ username, password }); if (username && password) users.push({ username, password, displayName });
} }
return users; return users;
} }
function adminProfile(username) {
return configuredUsers().find((user) => user.username === username) || { username, displayName: username };
}
async function adminLogin(req, res) { async function adminLogin(req, res) {
const body = await readJson(req); const body = await readJson(req);
const match = configuredUsers().find((u) => u.username === body.username && u.password === body.password); const match = configuredUsers().find((u) => u.username === body.username && u.password === body.password);
if (!match) return json(res, 401, { error: "bad_credentials" }); if (!match) return json(res, 401, { error: "bad_credentials" });
const token = signSession(match.username); const token = signSession(match.username);
res.setHeader("Set-Cookie", `${COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Path=/; Max-Age=${60 * 60 * 24 * 14}`); res.setHeader("Set-Cookie", `${COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Path=/; Max-Age=${60 * 60 * 24 * 14}`);
return json(res, 200, { ok: true, user: { username: match.username } }); return json(res, 200, { ok: true, user: { username: match.username, displayName: match.displayName } });
} }
function adminLogout(res) { function adminLogout(res) {
@@ -253,7 +258,7 @@ function requireAdmin(req, res, next) {
const token = parseCookies(req.headers.cookie || "")[COOKIE_NAME]; const token = parseCookies(req.headers.cookie || "")[COOKIE_NAME];
const username = token && verifySession(token); const username = token && verifySession(token);
if (!username) return json(res, 401, { error: "unauthorized" }); if (!username) return json(res, 401, { error: "unauthorized" });
req.adminUser = { username }; req.adminUser = adminProfile(username);
return next(); return next();
} }
@@ -423,7 +428,7 @@ async function createAdminMessage(req, res, url) {
if (!attachmentsAreAllowed(body.attachments || [])) return json(res, 400, { error: "only_images_allowed" }); if (!attachmentsAreAllowed(body.attachments || [])) return json(res, 400, { error: "only_images_allowed" });
const conversation = findConversation(publicIdFrom(url)); const conversation = findConversation(publicIdFrom(url));
if (!conversation) return json(res, 404, { error: "conversation_not_found" }); if (!conversation) return json(res, 404, { error: "conversation_not_found" });
const messageId = insertMessage(conversation.id, "admin", req.adminUser.username, String(body.message || "").trim()); const messageId = insertMessage(conversation.id, "admin", req.adminUser.displayName || req.adminUser.username, String(body.message || "").trim());
saveAttachments(conversation.id, messageId, body.attachments || []); saveAttachments(conversation.id, messageId, body.attachments || []);
bumpConversation(conversation.id, conversation.status === "new" ? "open" : conversation.status); bumpConversation(conversation.id, conversation.status === "new" ? "open" : conversation.status);
markConversationSeen(conversation.id); markConversationSeen(conversation.id);