diff --git a/admin-panel/src/main.jsx b/admin-panel/src/main.jsx index fd86a2d..57b18a3 100644 --- a/admin-panel/src/main.jsx +++ b/admin-panel/src/main.jsx @@ -1,63 +1,177 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { createRoot } from 'react-dom/client'; -import { RefreshCw, Server, ShieldCheck, Wifi } from 'lucide-react'; +import { + Activity, + Bot, + Clock, + MessageCircle, + RefreshCw, + Server, + ShieldCheck, + Users, + Wifi +} from 'lucide-react'; import './styles.css'; const API_URL = import.meta.env.VITE_API_URL || 'https://api.stackworks.ru'; +const REFRESH_MS = 15000; + +function formatDate(value) { + if (!value) return '—'; + const date = new Date(value.replace(' ', 'T') + 'Z'); + if (Number.isNaN(date.getTime())) return value; + return new Intl.DateTimeFormat('ru-RU', { + dateStyle: 'short', + timeStyle: 'short' + }).format(date); +} + +function StatCard({ icon: Icon, label, value }) { + return ( +
+
+
+

{label}

+ {value ?? 0} +
+
+ ); +} function App() { const [health, setHealth] = useState(null); + const [stats, setStats] = useState(null); + const [messages, setMessages] = useState([]); const [loading, setLoading] = useState(false); + const [lastUpdate, setLastUpdate] = useState(null); + const [error, setError] = useState(''); + + const healthOk = health?.ok === true; + + const statCards = useMemo(() => ([ + { icon: ShieldCheck, label: 'Сайты', value: stats?.sites }, + { icon: Users, label: 'Посетители', value: stats?.visitors }, + { icon: MessageCircle, label: 'Диалоги', value: stats?.conversations }, + { icon: Activity, label: 'Открытые', value: stats?.openConversations }, + { icon: Bot, label: 'Сообщения', value: stats?.messages } + ]), [stats]); + + async function loadDashboard({ silent = false } = {}) { + if (!silent) setLoading(true); + setError(''); - async function checkHealth() { - setLoading(true); try { - const response = await fetch(API_URL + '/health'); - setHealth(await response.json()); - } catch (error) { - setHealth({ ok: false, error: error.message }); + const [healthResponse, statsResponse, messagesResponse] = await Promise.all([ + fetch(API_URL + '/health'), + fetch(API_URL + '/api/admin/stats'), + fetch(API_URL + '/api/admin/messages?limit=25') + ]); + + const [healthData, statsData, messagesData] = await Promise.all([ + healthResponse.json(), + statsResponse.json(), + messagesResponse.json() + ]); + + setHealth(healthData); + setStats(statsData.stats || null); + setMessages(Array.isArray(messagesData.messages) ? messagesData.messages : []); + setLastUpdate(new Date()); + } catch (requestError) { + setHealth({ ok: false, error: requestError.message }); + setError(requestError.message); } finally { - setLoading(false); + if (!silent) setLoading(false); } } useEffect(() => { - checkHealth(); + loadDashboard(); + const timer = setInterval(() => loadDashboard({ silent: true }), REFRESH_MS); + return () => clearInterval(timer); }, []); return (
-

WSChat

-

Admin panel

-

VPS backend, website widget, Telegram operators.

+

WSChat · operator console

+

Панель управления

+

Сообщения с сайта, статистика, Telegram-операторы и будущие настройки виджета в одном месте.

+
+ + API: {healthOk ? 'работает' : 'ошибка'} + + + {lastUpdate ? `обновлено ${formatDate(lastUpdate.toISOString())}` : 'ожидает данных'} + +
-
-
-
- -

Backend

-

API: {API_URL}

-
{JSON.stringify(health, null, 2)}
+ {error ?
Ошибка API: {error}
: null} + +
+ {statCards.map((card) => )} +
+ +
+
+
+
+

Inbox

+

Последние сообщения

+
+ {messages.length} +
+ + {messages.length === 0 ? ( +
+ +

Сообщений пока нет. После отправки из виджета они появятся здесь.

+
+ ) : ( +
+ {messages.map((message) => ( +
+
+ {message.visitor_key || message.direction} + {formatDate(message.created_at)} +
+

{message.body}

+
+ {message.site_domain || message.site_name || message.site_id} + {message.conversation_id} +
+
+ ))} +
+ )}
-
- -

FastPanel-safe deploy

-

Deploy updates only widget/admin static files inside the configured webroot.

-
+
);