MOHAN aef977eb99 feat: customer chat system with admin panel inbox and embeddable widget
- chatStore.js: per-shop JSON storage in data/chats/, message history,
  unread tracking, markRead on admin open
- adminPanel.js: new Chat tab (WhatsApp-style left sidebar + right panel),
  3s polling, admin reply, unread badge on tab
- server.js: public POST /chat/:shop and GET /chat/:shop for widget,
  GET /chat/widget.js serves embeddable script
- Widget: floating chat button, popup window, customer sends messages,
  polls for admin replies, visitor ID persisted in localStorage
  Usage: <script src="https://backend.data4autos.com/chat/widget.js?shop=SHOP"></script>

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 20:45:18 +05:30

70 lines
2.1 KiB
JavaScript

// chatStore.js — per-shop customer chat message storage
const fs = require('fs');
const path = require('path');
const { v4: uuid } = require('uuid');
const chatsDir = path.resolve(__dirname, 'data', 'chats');
if (!fs.existsSync(chatsDir)) fs.mkdirSync(chatsDir, { recursive: true });
function safeKey(shop) {
return shop.toLowerCase().trim().replace(/[^a-z0-9.-]/g, '_');
}
function chatFile(shop) {
return path.join(chatsDir, safeKey(shop) + '.json');
}
function readChat(shop) {
const f = chatFile(shop);
if (!fs.existsSync(f)) return { shop, messages: [] };
try { return JSON.parse(fs.readFileSync(f, 'utf8')); }
catch { return { shop, messages: [] }; }
}
function saveChat(shop, data) {
fs.writeFileSync(chatFile(shop), JSON.stringify(data, null, 2), 'utf8');
}
function addMessage(shop, from, text, visitorId = null) {
const chat = readChat(shop);
const msg = {
id: uuid(),
from, // 'customer' | 'admin'
text: String(text).trim().slice(0, 2000),
timestamp: new Date().toISOString(),
read: from === 'admin',
visitorId: visitorId || null,
};
chat.messages.push(msg);
saveChat(shop, chat);
return msg;
}
function markRead(shop) {
const chat = readChat(shop);
let changed = false;
chat.messages.forEach(m => {
if (m.from === 'customer' && !m.read) { m.read = true; changed = true; }
});
if (changed) saveChat(shop, chat);
}
function listChats() {
if (!fs.existsSync(chatsDir)) return [];
return fs.readdirSync(chatsDir)
.filter(f => f.endsWith('.json'))
.map(f => {
try {
const data = JSON.parse(fs.readFileSync(path.join(chatsDir, f), 'utf8'));
const msgs = data.messages || [];
const last = msgs[msgs.length - 1] || null;
const unread = msgs.filter(m => m.from === 'customer' && !m.read).length;
return { shop: data.shop, lastMessage: last, unread, messageCount: msgs.length };
} catch { return null; }
})
.filter(Boolean)
.sort((a, b) => (b.lastMessage?.timestamp || '').localeCompare(a.lastMessage?.timestamp || ''));
}
module.exports = { addMessage, readChat, markRead, listChats };