E3 UI: дашборд «Активные поиски» — главная страница после логина

- Dashboard.jsx: счётчики (активных / всего / завершено за 24ч из
  /operations/summary), фильтры по статусу, сетка карточек операций
  (title, статус-бейдж, подразделение, время обновления), клик →
  рабочее пространство (анализ по карточке операции).
- Роут /dashboard, «/» → дашборд (было /desktop).
- CaseForm: после создания карточки автоматически создаётся поисковая
  операция (title «Поиск: имя, N лет») — операция не блокирует анализ.
- VCL-density, палитра ЦОУ (#ff4d00 акценты, статусы цветом).
This commit is contained in:
2026-09-09 21:33:01 +03:00
parent 5c3b8a1424
commit ab3eebe274
4 changed files with 294 additions and 1 deletions
+129
View File
@@ -0,0 +1,129 @@
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { apiFetch } from "../api/client";
import "./Dashboard.css";
const STATUS_LABEL = {
planned: 'Запланирован',
active: 'Активен',
paused: 'Приостановлен',
completed: 'Завершён',
archived: 'В архиве',
};
const STATUS_CLASS = {
planned: 'st-planned',
active: 'st-active',
paused: 'st-paused',
completed: 'st-completed',
archived: 'st-archived',
};
const Dashboard = () => {
const navigate = useNavigate();
const [operations, setOperations] = useState([]);
const [summary, setSummary] = useState({ active: 0, total: 0, completed_24h: 0 });
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [statusFilter, setStatusFilter] = useState('active');
const [creating, setCreating] = useState(false);
const load = async () => {
try {
setLoading(true);
setError(null);
const qs = statusFilter ? `?status=${statusFilter}` : '';
const resp = await apiFetch(`/api/v1/operations${qs}`);
if (!resp.ok) throw new Error('Не удалось загрузить поиски');
const data = await resp.json();
setOperations(data.items);
const sumResp = await apiFetch('/api/v1/operations/summary');
if (sumResp.ok) setSummary(await sumResp.json());
} catch (e) {
setError(e.message);
} finally {
setLoading(false);
}
};
useEffect(() => { load(); }, [statusFilter]);
const openCase = (op) => {
// Рабочее пространство операции: анализ по её карточке
navigate(`/analysis/${op.case_id}`, { state: { operation: op } });
};
const startNewSearch = () => navigate('/desktop');
return (
<div className="dashboard">
<div className="dash-header">
<h1><span className="logo">ВЕКТОР</span> Активные поиски</h1>
<button className="dash-new-btn" onClick={startNewSearch}>+ Новый поиск</button>
</div>
<div className="dash-counters">
<div className="counter-card counter-active">
<div className="counter-value">{summary.active}</div>
<div className="counter-label">Активных</div>
</div>
<div className="counter-card counter-total">
<div className="counter-value">{summary.total}</div>
<div className="counter-label">Всего операций</div>
</div>
<div className="counter-card counter-closed">
<div className="counter-value">{summary.completed_24h}</div>
<div className="counter-label">Завершено за 24 ч</div>
</div>
</div>
<div className="dash-filters">
{['active', 'paused', 'completed', '', ].map((s) => (
<button
key={s || 'all'}
className={`dash-filter ${statusFilter === s ? 'selected' : ''}`}
onClick={() => setStatusFilter(s)}
>
{s ? STATUS_LABEL[s] : 'Все'}
</button>
))}
</div>
{loading && <div className="dash-loading">Загрузка...</div>}
{error && <div className="error-banner">{error}</div>}
{!loading && !error && operations.length === 0 && (
<div className="dash-empty">
{statusFilter === 'active'
? 'Активных поисков нет. Создайте новый поиск.'
: 'Операций в этом статусе нет.'}
</div>
)}
{!loading && !error && (
<div className="dash-grid">
{operations.map((op) => (
<div key={op.id} className="op-card" onClick={() => openCase(op)}>
<div className={`op-status ${STATUS_CLASS[op.status] || ''}`}>
{STATUS_LABEL[op.status] || op.status}
</div>
<div className="op-title">{op.title}</div>
<div className="op-meta">
{op.created_by_name && <span className="op-unit">{op.created_by_name}</span>}
{op.updated_at && (
<span className="op-time">
обновлено {new Date(op.updated_at).toLocaleString('ru-RU', {
day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit',
})}
</span>
)}
</div>
</div>
))}
</div>
)}
</div>
);
};
export default Dashboard;