184 lines
9.9 KiB
HTML
184 lines
9.9 KiB
HTML
<!doctype html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>CRM Dashboard — ДРБ</title>
|
||
<style>
|
||
* { margin: 0; box-sizing: border-box; }
|
||
body { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; background: #0f1218; color: #f4f4f5; padding: 20px; }
|
||
.container { max-width: 1200px; margin: 0 auto; }
|
||
h1 { font-size: 28px; margin-bottom: 10px; }
|
||
h2 { font-size: 20px; margin: 30px 0 15px; color: #00b8cc; }
|
||
.presets { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 20px; }
|
||
.preset-btn { background: #1a1d26; border: 1px solid #333; color: #f4f4f5; padding: 12px 20px; border-radius: 8px; cursor: pointer; font-size: 14px; transition: all 0.2s; }
|
||
.preset-btn:hover { border-color: #00b8cc; background: #1f2430; }
|
||
.preset-btn.primary { background: #00b8cc; border-color: #00b8cc; color: #0f1218; font-weight: 600; }
|
||
.preset-btn.primary:hover { background: #00d4ee; }
|
||
.custom-query { display: flex; gap: 10px; margin-bottom: 30px; }
|
||
.custom-query input { flex: 1; padding: 12px 16px; border: 1px solid #333; border-radius: 8px; background: #1a1d26; color: #f4f4f5; font-size: 14px; }
|
||
.custom-query input:focus { outline: none; border-color: #00b8cc; }
|
||
.custom-query button { padding: 12px 24px; background: #00b8cc; border: none; border-radius: 8px; color: #0f1218; font-weight: 600; cursor: pointer; }
|
||
.status-bar { background: #1a1d26; padding: 15px; border-radius: 8px; margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; }
|
||
.status-item { display: flex; align-items: center; gap: 8px; }
|
||
.status-dot { width: 10px; height: 10px; border-radius: 50%; }
|
||
.status-dot.pending { background: #f59e0b; }
|
||
.status-dot.completed { background: #10b981; }
|
||
.status-dot.error { background: #ef4444; }
|
||
.requests { display: flex; flex-direction: column; gap: 15px; }
|
||
.request-card { background: #1a1d26; border: 1px solid #333; border-radius: 10px; padding: 16px; }
|
||
.request-card.pending { border-left: 4px solid #f59e0b; }
|
||
.request-card.completed { border-left: 4px solid #10b981; }
|
||
.request-card.error { border-left: 4px solid #ef4444; }
|
||
.request-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
|
||
.request-query { font-weight: 500; color: #e4e4e7; }
|
||
.request-meta { font-size: 12px; color: #71717a; }
|
||
.request-response { background: #0f1218; padding: 12px; border-radius: 6px; font-family: monospace; font-size: 13px; white-space: pre-wrap; color: #a1a1aa; max-height: 400px; overflow-y: auto; }
|
||
.empty-state { text-align: center; padding: 60px 20px; color: #71717a; }
|
||
.clear-btn { background: transparent; border: 1px solid #ef4444; color: #ef4444; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 13px; }
|
||
.clear-btn:hover { background: #ef4444; color: #fff; }
|
||
.loading { animation: pulse 1.5s infinite; }
|
||
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
|
||
.badge { display: inline-block; padding: 4px 10px; border-radius: 20px; font-size: 11px; font-weight: 600; text-transform: uppercase; }
|
||
.badge-pending { background: #f59e0b; color: #0f1218; }
|
||
.badge-completed { background: #10b981; color: #0f1218; }
|
||
.badge-error { background: #ef4444; color: #fff; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>📊 CRM Dashboard — ДРБ</h1>
|
||
<p style="color: #71717a; margin-bottom: 20px;">Панель запросов к Alem-агенту по данным CRM</p>
|
||
|
||
<h2>Готовые запросы</h2>
|
||
<div class="presets">
|
||
<button class="preset-btn primary" onclick="sendQuery('Продажи за вчерашний день по ДРБ: количество, сумма, топ продуктов', 'Продажи ДРБ')">📈 Продажи ДРБ за вчера</button>
|
||
<button class="preset-btn" onclick="sendQuery('Воронка продаж по филиалам ДРБ за неделю: количество заказов по статусам', 'Воронка')">🔄 Воронка продаж</button>
|
||
<button class="preset-btn" onclick="sendQuery('Топ 10 продуктов по количеству продаж за последние 7 дней', 'Топ продуктов')">🏆 Топ продуктов</button>
|
||
<button class="preset-btn" onclick="sendQuery('Количество отказов по причинам за вчера', 'Отказы')">❌ Отказы</button>
|
||
<button class="preset-btn" onclick="sendQuery('Сравнение продаж этой недели с прошлой неделей по ДРБ', 'Сравнение')">📊 Сравнение недель</button>
|
||
<button class="preset-btn" onclick="sendQuery('Статусы заказов по каналам продаж за вчера', 'Каналы')">📱 Каналы продаж</button>
|
||
</div>
|
||
|
||
<h2>Свой запрос</h2>
|
||
<div class="custom-query">
|
||
<input type="text" id="customQuery" placeholder="Напишите свой запрос к агенту..." onkeypress="if(event.key==='Enter') sendCustom()">
|
||
<button onclick="sendCustom()">Отправить</button>
|
||
</div>
|
||
|
||
<div class="status-bar">
|
||
<div class="status-item">
|
||
<span class="status-dot" id="statusDot"></span>
|
||
<span id="statusText">Ожидание запросов...</span>
|
||
</div>
|
||
<button class="clear-btn" onclick="clearData()">Очистить историю</button>
|
||
</div>
|
||
|
||
<h2>История запросов</h2>
|
||
<div id="requestsList" class="requests">
|
||
<div class="empty-state">Нет запросов. Выберите готовый preset или напишите свой запрос.</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const PRESET_LABELS = {
|
||
'Продажи ДРБ': '📈 Продажи ДРБ',
|
||
'Воронка': '🔄 Воронка',
|
||
'Топ продуктов': '🏆 Топ продуктов',
|
||
'Отказы': '❌ Отказы',
|
||
'Сравнение': '📊 Сравнение',
|
||
'Каналы': '📱 Каналы'
|
||
};
|
||
|
||
async function sendQuery(query, preset) {
|
||
const resp = await fetch("/api/request", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ query, preset })
|
||
});
|
||
const data = await resp.json();
|
||
if (data.ok) {
|
||
document.getElementById("customQuery").value = "";
|
||
loadRequests();
|
||
}
|
||
}
|
||
|
||
function sendCustom() {
|
||
const query = document.getElementById("customQuery").value.trim();
|
||
if (query) sendQuery(query, null);
|
||
}
|
||
|
||
async function loadRequests() {
|
||
const resp = await fetch("/api/requests");
|
||
const data = await resp.json();
|
||
render(data.requests, data.responses);
|
||
}
|
||
|
||
function render(requests, responses) {
|
||
const container = document.getElementById("requestsList");
|
||
if (!requests || requests.length === 0) {
|
||
container.innerHTML = '<div class="empty-state">Нет запросов. Выберите готовый preset или напишите свой запрос.</div>';
|
||
updateStatus("Ожидание запросов...", null);
|
||
return;
|
||
}
|
||
|
||
const respMap = {};
|
||
(responses || []).forEach(r => { respMap[r.id] = r; });
|
||
|
||
const pending = requests.filter(r => r.status === "pending").length;
|
||
const statusText = pending > 0
|
||
? `Ожидает ответа: ${pending} запрос(ов)`
|
||
: "Все запросы обработаны";
|
||
const statusClass = pending > 0 ? "pending" : (requests.length > 0 ? "completed" : null);
|
||
updateStatus(statusText, statusClass);
|
||
|
||
container.innerHTML = requests.map(r => {
|
||
const resp = respMap[r.id];
|
||
const badgeClass = r.status === "pending" ? "badge-pending" : r.status === "error" ? "badge-error" : "badge-completed";
|
||
const badgeText = r.status === "pending" ? "В обработке" : r.status === "error" ? "Ошибка" : "Готово";
|
||
const presetLabel = r.preset ? PRESET_LABELS[r.preset] || r.preset : null;
|
||
|
||
return `
|
||
<div class="request-card ${r.status}">
|
||
<div class="request-header">
|
||
<div>
|
||
${presetLabel ? '<span style="font-size:12px;color:#00b8cc;margin-right:8px;">'+presetLabel+'</span>' : ''}
|
||
<span class="request-query">${escapeHtml(r.query)}</span>
|
||
</div>
|
||
<span class="badge ${badgeClass}">${badgeText}</span>
|
||
</div>
|
||
<div class="request-meta">
|
||
${new Date(r.created).toLocaleString("ru-RU")}
|
||
${resp && resp.updated ? ' • Ответ: ' + new Date(resp.updated).toLocaleString("ru-RU") : ''}
|
||
</div>
|
||
${resp && resp.response ? '<div class="request-response">' + escapeHtml(resp.response) + '</div>' : ''}
|
||
${r.status === "pending" ? '<div class="request-response loading">⏳ Запрос отправлен агенту... Ожидайте ответа в чате.</div>' : ''}
|
||
</div>
|
||
`;
|
||
}).join("");
|
||
}
|
||
|
||
function updateStatus(text, statusClass) {
|
||
document.getElementById("statusText").textContent = text;
|
||
const dot = document.getElementById("statusDot");
|
||
dot.className = "status-dot " + (statusClass || "");
|
||
}
|
||
|
||
function escapeHtml(str) {
|
||
if (!str) return "";
|
||
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||
}
|
||
|
||
async function clearData() {
|
||
if (!confirm("Очистить всю историю запросов?")) return;
|
||
await fetch("/api/clear", { method: "DELETE" });
|
||
loadRequests();
|
||
}
|
||
|
||
// Автообновление каждые 5 секунд
|
||
setInterval(loadRequests, 5000);
|
||
loadRequests();
|
||
</script>
|
||
</body>
|
||
</html>
|