Files
vector/frontend/src/pages/Dashboard.jsx
T
Sadmin 39b2e296d1 E3 UI: админка пользователей — создание, роли, подразделения, блокировка, сброс пароля
- UsersAdmin.jsx (/users): список с фильтрами (подразделение/поиск),
  форма создания (логин/email/временный пароль/ФИО/должность/юнит/роль),
  inline-смена роли и подразделения, отключение/включение, сброс пароля
  (все сессии отзываются), статусы цветом. must_change_password
  подсвечивается пользователю при первом входе.
- Доступ: бэкенд require_permission('manage_users') — РЦУ РЧС и ОУМЧС;
  скоуп координатора фильтрует список по подразделению (кросс-областной
  PATCH отбивается 403 на бэкенде).
- Кнопка «Пользователи» из дашборда.
2026-09-09 21:42:30 +03:00

136 lines
4.8 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
<div style={{ display: 'flex', gap: '8px' }}>
<button className="dash-new-btn" onClick={startNewSearch}>+ Новый поиск</button>
<button
className="dash-new-btn"
style={{ background: '#21262d' }}
onClick={() => navigate('/users')}
>Пользователи</button>
</div>
</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;