diff --git a/.vibe42-run.log b/.vibe42-run.log index fa50074..20a5a1d 100644 --- a/.vibe42-run.log +++ b/.vibe42-run.log @@ -2,3 +2,7 @@ up to date in 393ms Dashboard running on port 3000 +--- run Thu Aug 27 06:53:37 AM UTC 2026 :: npm start --silent + +up to date in 333ms +Dashboard running on port 3000 diff --git a/.vibe42-run.pid b/.vibe42-run.pid index 8cf5c1a..f61b514 100644 --- a/.vibe42-run.pid +++ b/.vibe42-run.pid @@ -1 +1 @@ -86 +578 diff --git a/index.html b/index.html index 4470ec2..11684a4 100644 --- a/index.html +++ b/index.html @@ -3,31 +3,45 @@ -Дашборд заказов +Дашборд заказов - Аналитика @@ -75,6 +91,41 @@ body{font:14px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,s
Отказы
0
+
+
Конверсия
+
0%
+
+ + +
+
+

📈 Динамика по дням

+
+
+
+
+
+

📊 Статусы заказов

+
+
+
+
+
+
+ +
+
+

📱 Каналы продаж

+
+
+
+
+
+

👥 Топ операторов

+
+
+
+
@@ -166,19 +217,142 @@ function getStatusClass(status) { return ''; } +function extractDateFromId(id) { + const idStr = String(id); + if (idStr.length >= 6) { + const day = idStr.slice(4, 6); + const month = idStr.slice(2, 4); + const year = '20' + idStr.slice(0, 2); + return year + '-' + month + '-' + day; + } + return new Date().toISOString().slice(0, 10); +} + +function groupByDate(orders) { + const groups = {}; + orders.forEach(o => { + const date = extractDateFromId(o.id); + if (!groups[date]) groups[date] = 0; + groups[date]++; + }); + return Object.entries(groups).sort((a, b) => a[0].localeCompare(b[0])); +} + +function groupByChannel(orders) { + const groups = {}; + orders.forEach(o => { + const ch = o.channel || 'Без канала'; + if (!groups[ch]) groups[ch] = 0; + groups[ch]++; + }); + return Object.entries(groups).sort((a, b) => b[1] - a[1]); +} + +function groupByOperator(orders) { + const groups = {}; + orders.forEach(o => { + const op = o.operator || 'Без оператора'; + if (!groups[op]) groups[op] = 0; + groups[op]++; + }); + return Object.entries(groups).sort((a, b) => b[1] - a[1]).slice(0, 10); +} + +function groupByStatus(orders) { + const groups = {'в работе': 0, 'выполнен': 0, 'ОТКАЗ': 0}; + orders.forEach(o => { + if (groups[o.status] !== undefined) groups[o.status]++; + }); + return groups; +} + +function renderDailyChart(data) { + const container = document.getElementById("daily-chart"); + if (data.length === 0) { + container.innerHTML = '
Нет данных
'; + return; + } + const max = Math.max(...data.map(d => d[1])); + container.innerHTML = data.map(d => { + const height = (d[1] / max) * 180; + const date = d[0].slice(5) + '.' + d[0].slice(0, 4); + return '
' + d[1] + '' + date + '
'; + }).join(""); +} + +function renderChannelChart(data) { + const container = document.getElementById("channel-chart"); + if (data.length === 0) { + container.innerHTML = '
Нет данных
'; + return; + } + const max = Math.max(...data.map(d => d[1])); + container.innerHTML = data.map(d => { + const height = (d[1] / max) * 180; + return '
' + d[1] + '' + d[0].slice(0, 8) + '
'; + }).join(""); +} + +function renderOperatorChart(data) { + const container = document.getElementById("operator-chart"); + if (data.length === 0) { + container.innerHTML = '
Нет данных
'; + return; + } + const max = Math.max(...data.map(d => d[1])); + container.innerHTML = data.map(d => { + const height = (d[1] / max) * 180; + return '
' + d[1] + '' + d[0].slice(0, 10) + '
'; + }).join(""); +} + +function renderStatusPie(statuses) { + const total = statuses['в работе'] + statuses['выполнен'] + statuses['ОТКАЗ']; + if (total === 0) return; + + const activeDeg = (statuses['в работе'] / total) * 360; + const completedDeg = (statuses['выполнен'] / total) * 360; + const rejectedDeg = (statuses['ОТКАЗ'] / total) * 360; + + const pie = document.getElementById("status-pie"); + pie.style.background = 'conic-gradient(#F59E0B 0deg ' + activeDeg + 'deg, #10B981 ' + activeDeg + 'deg ' + (activeDeg + completedDeg) + 'deg, #EF4444 ' + (activeDeg + completedDeg) + 'deg 360deg)'; + + const legend = document.getElementById("status-legend"); + legend.innerHTML = [ + {label: 'В работе', count: statuses['в работе'], color: '#F59E0B'}, + {label: 'Выполнен', count: statuses['выполнен'], color: '#10B981'}, + {label: 'ОТКАЗ', count: statuses['ОТКАЗ'], color: '#EF4444'} + ].map(item => '
' + item.label + ': ' + item.count + '
').join(""); +} + async function load() { const res = await fetch("/api/data"); const data = await res.json(); allOrders = data.orders || []; renderTable(allOrders); updateStats(allOrders); + updateCharts(allOrders); } function updateStats(orders) { - document.getElementById("stat-total").textContent = orders.length; - document.getElementById("stat-active").textContent = orders.filter(o => o.status === "в работе").length; - document.getElementById("stat-completed").textContent = orders.filter(o => o.status === "выполнен").length; - document.getElementById("stat-rejected").textContent = orders.filter(o => o.status === "ОТКАЗ").length; + const total = orders.length; + const active = orders.filter(o => o.status === "в работе").length; + const completed = orders.filter(o => o.status === "выполнен").length; + const rejected = orders.filter(o => o.status === "ОТКАЗ").length; + const conversion = total > 0 ? Math.round((completed / total) * 100) : 0; + + document.getElementById("stat-total").textContent = total; + document.getElementById("stat-active").textContent = active; + document.getElementById("stat-completed").textContent = completed; + document.getElementById("stat-rejected").textContent = rejected; + document.getElementById("stat-conversion").textContent = conversion + '%'; +} + +function updateCharts(orders) { + renderDailyChart(groupByDate(orders)); + renderChannelChart(groupByChannel(orders)); + renderOperatorChart(groupByOperator(orders)); + renderStatusPie(groupByStatus(orders)); } function renderTable(orders) {