feat: apply new Мои Товары design system across all templates

Replace Pico CSS with custom design: dark sidebar layout, Golos Text +
JetBrains Mono fonts, orange accent (#FF5500), new component classes
(cards, tables, buttons, tags, toggles, alerts, tabs, login split-panel).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mguschin
2026-05-18 12:43:08 +03:00
parent 75513e647d
commit eb4165e48b
25 changed files with 2201 additions and 1889 deletions

View File

@@ -1,147 +1,147 @@
{% extends "base.html" %}
{% block title %}API Логи — ЭВОСИНК{% endblock %}
{% block title %}API Логи — Мои Товары{% endblock %}
{% block page_title %}API Логи{% endblock %}
{% block content %}
<div class="d-flex justify-between align-center mb-3">
<h1 style="font-size:1.3rem; margin:0;"><i class="bi bi-journal-text me-2"></i>API Логи</h1>
<span class="text-muted small">Найдено: {{ total }}</span>
<div class="pg-title">API Логи</div>
<div class="pg-sub">Журнал всех исходящих запросов · Найдено: {{ total }}</div>
<!-- Filters -->
<div class="card" style="margin-bottom:14px;padding:14px 20px;">
<form method="get" action="/admin/logs" style="display:flex;flex-wrap:wrap;gap:8px;align-items:center;">
<select class="inp" name="service" style="width:auto;">
<option value="" {% if not filter_service %}selected{% endif %}>Все сервисы</option>
<option value="evotor" {% if filter_service == 'evotor' %}selected{% endif %}>Эвотор</option>
<option value="vk" {% if filter_service == 'vk' %}selected{% endif %}>ВКонтакте</option>
<option value="other" {% if filter_service == 'other' %}selected{% endif %}>Другое</option>
</select>
<select class="inp" name="method" style="width:auto;">
<option value="" {% if not filter_method %}selected{% endif %}>Все методы</option>
<option value="GET" {% if filter_method == 'GET' %}selected{% endif %}>GET</option>
<option value="POST" {% if filter_method == 'POST' %}selected{% endif %}>POST</option>
</select>
<select class="inp" name="status" style="width:auto;">
<option value="" {% if not filter_status %}selected{% endif %}>Любой статус</option>
<option value="ok" {% if filter_status == 'ok' %}selected{% endif %}>2xx / 3xx</option>
<option value="error" {% if filter_status == 'error' %}selected{% endif %}>4xx / 5xx</option>
<option value="200" {% if filter_status == '200' %}selected{% endif %}>200</option>
<option value="401" {% if filter_status == '401' %}selected{% endif %}>401</option>
<option value="403" {% if filter_status == '403' %}selected{% endif %}>403</option>
<option value="429" {% if filter_status == '429' %}selected{% endif %}>429</option>
<option value="500" {% if filter_status == '500' %}selected{% endif %}>500</option>
</select>
<select class="inp" name="hours" style="width:auto;">
<option value="1" {% if filter_hours == 1 %}selected{% endif %}>Последний час</option>
<option value="6" {% if filter_hours == 6 %}selected{% endif %}>6 часов</option>
<option value="24" {% if filter_hours == 24 %}selected{% endif %}>24 часа</option>
<option value="168" {% if filter_hours == 168 or (not filter_hours) %}selected{% endif %}>7 дней</option>
<option value="720" {% if filter_hours == 720 %}selected{% endif %}>30 дней</option>
</select>
<input class="inp" type="search" name="q" value="{{ filter_q }}"
placeholder="URL или тело ответа…" style="flex:1;min-width:160px;">
<button type="submit" class="btn btn-primary btn-sm">Применить</button>
{% if filter_service or filter_method or filter_status or filter_q or filter_hours != 24 %}
<a href="/admin/logs" class="btn btn-outline btn-sm">Сбросить</a>
{% endif %}
</form>
</div>
{# ── filters ── #}
<form method="get" action="/admin/logs" class="mb-3" style="display:flex; flex-wrap:wrap; gap:0.5rem; align-items:center;">
<select name="service" style="width:auto;">
<option value="" {% if not filter_service %}selected{% endif %}>Все сервисы</option>
<option value="evotor" {% if filter_service == 'evotor' %}selected{% endif %}>Эвотор</option>
<option value="vk" {% if filter_service == 'vk' %}selected{% endif %}>ВКонтакте</option>
<option value="other" {% if filter_service == 'other' %}selected{% endif %}>Другое</option>
</select>
<select name="method" style="width:auto;">
<option value="" {% if not filter_method %}selected{% endif %}>Все методы</option>
<option value="GET" {% if filter_method == 'GET' %}selected{% endif %}>GET</option>
<option value="POST" {% if filter_method == 'POST' %}selected{% endif %}>POST</option>
</select>
<select name="status" style="width:auto;">
<option value="" {% if not filter_status %}selected{% endif %}>Любой статус</option>
<option value="ok" {% if filter_status == 'ok' %}selected{% endif %}>2xx / 3xx</option>
<option value="error" {% if filter_status == 'error' %}selected{% endif %}>4xx / 5xx</option>
<option value="200" {% if filter_status == '200' %}selected{% endif %}>200</option>
<option value="401" {% if filter_status == '401' %}selected{% endif %}>401</option>
<option value="403" {% if filter_status == '403' %}selected{% endif %}>403</option>
<option value="429" {% if filter_status == '429' %}selected{% endif %}>429</option>
<option value="500" {% if filter_status == '500' %}selected{% endif %}>500</option>
</select>
<select name="hours" style="width:auto;">
<option value="1" {% if filter_hours == 1 %}selected{% endif %}>Последний час</option>
<option value="6" {% if filter_hours == 6 %}selected{% endif %}>6 часов</option>
<option value="24" {% if filter_hours == 24 %}selected{% endif %}>24 часа</option>
<option value="168" {% if filter_hours == 168 or (not filter_hours) %}selected{% endif %}>7 дней</option>
<option value="720" {% if filter_hours == 720 %}selected{% endif %}>30 дней</option>
</select>
<input type="search" name="q" value="{{ filter_q }}" placeholder="URL или тело ответа…" style="flex:1; min-width:160px;">
<button type="submit">Применить</button>
{% if filter_service or filter_method or filter_status or filter_q or filter_hours != 24 %}
<a href="/admin/logs" role="button" class="outline secondary">Сбросить</a>
<div class="card" style="padding:0;">
{% if logs %}
<div class="table-wrap">
<table class="tbl" style="font-size:12px;">
<thead>
<tr>
<th style="width:150px;">Время</th>
<th style="width:80px;">Сервис</th>
<th style="width:50px;">Метод</th>
<th style="width:60px;">Статус</th>
<th style="width:70px;">Мс</th>
<th>URL</th>
<th style="width:32px;"></th>
</tr>
</thead>
<tbody>
{% for log in logs %}
{% set is_error = log.response_status and log.response_status >= 400 %}
<tr style="cursor:pointer;" onclick="toggleDetail({{ log.id }})">
<td><span class="mono" style="font-size:11px;color:#9EA8BE;">{{ log.created_at | datefmt }}</span></td>
<td>
{% if log.service == 'evotor' %}
<span class="tag tag-bl" style="font-size:10px;padding:2px 6px;">{{ log.service }}</span>
{% elif log.service == 'vk' %}
<span class="tag tag-or" style="font-size:10px;padding:2px 6px;">{{ log.service }}</span>
{% else %}
<span class="tag tag-dim" style="font-size:10px;padding:2px 6px;">{{ log.service }}</span>
{% endif %}
</td>
<td><span class="mono" style="font-size:11px;">{{ log.method }}</span></td>
<td>
{% if log.response_status %}
<span class="mono" style="font-size:11px;color:{% if is_error %}#E53935{% else %}#17A865{% endif %};">{{ log.response_status }}</span>
{% else %}
<span style="color:#9EA8BE;"></span>
{% endif %}
</td>
<td><span class="mono" style="font-size:11px;color:#9EA8BE;">{{ log.duration_ms if log.duration_ms is not none else '—' }}</span></td>
<td style="max-width:400px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
<span class="mono" style="font-size:11px;{% if is_error %}color:#E53935;{% endif %}" title="{{ log.url }}">{{ log.url }}</span>
</td>
<td style="color:#9EA8BE;"><i class="bi bi-chevron-down"></i></td>
</tr>
<tr id="detail-{{ log.id }}" style="display:none;background:#F9FAFB;">
<td colspan="7" style="padding:14px 20px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
<div>
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:0.06em;color:#9EA8BE;margin-bottom:6px;">URL</div>
<code style="word-break:break-all;font-size:11px;">{{ log.url }}</code>
{% if log.request_body %}
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:0.06em;color:#9EA8BE;margin:10px 0 6px;">Request body</div>
<pre style="font-size:11px;max-height:180px;overflow:auto;">{{ log.request_body }}</pre>
{% endif %}
</div>
<div>
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:0.06em;color:#9EA8BE;margin-bottom:6px;">Response ({{ log.response_status }})</div>
{% if log.response_body %}
<pre style="font-size:11px;max-height:180px;overflow:auto;">{{ log.response_body }}</pre>
{% else %}
<span style="color:#9EA8BE;font-size:12px;"></span>
{% endif %}
</div>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if total_pages > 1 %}
<div class="pagination">
{% if page > 1 %}
<a href="?service={{ filter_service }}&method={{ filter_method }}&status={{ filter_status }}&q={{ filter_q }}&hours={{ filter_hours }}&page={{ page - 1 }}" class="btn btn-outline btn-sm">← Назад</a>
{% endif %}
</form>
<article class="card" style="padding:0;">
{% if logs %}
<div class="table-scroll">
<table class="align-middle" style="font-size:0.82rem;">
<thead>
<tr>
<th style="width:140px;">Время</th>
<th style="width:60px;">Сервис</th>
<th style="width:40px;">Метод</th>
<th style="width:50px;">Статус</th>
<th style="width:60px;">Мс</th>
<th>URL</th>
<th style="width:30px;"></th>
</tr>
</thead>
<tbody>
{% for log in logs %}
{% set is_error = log.response_status and log.response_status >= 400 %}
<tr class="{{ 'text-danger' if is_error else '' }}" style="cursor:pointer;" onclick="toggleDetail({{ log.id }})">
<td class="text-muted">{{ log.created_at | datefmt }}</td>
<td>
<span class="badge {{ 'badge-evotor' if log.service == 'evotor' else 'badge-vk' if log.service == 'vk' else '' }}">
{{ log.service }}
</span>
</td>
<td><code>{{ log.method }}</code></td>
<td>
{% if log.response_status %}
<span class="{{ 'text-danger' if is_error else 'text-muted' }}">{{ log.response_status }}</span>
{% else %}
<span class="text-muted"></span>
{% endif %}
</td>
<td class="text-muted">{{ log.duration_ms if log.duration_ms is not none else '—' }}</td>
<td style="max-width:400px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">
<span title="{{ log.url }}">{{ log.url }}</span>
</td>
<td class="text-muted"><i class="bi bi-chevron-down"></i></td>
</tr>
<tr id="detail-{{ log.id }}" style="display:none; background:var(--pico-card-background-color);">
<td colspan="7" style="padding:0.75rem 1rem;">
<div style="display:grid; grid-template-columns:1fr 1fr; gap:1rem;">
<div>
<div class="text-muted small mb-1"><strong>URL</strong></div>
<code style="word-break:break-all; font-size:0.78rem;">{{ log.url }}</code>
{% if log.request_body %}
<div class="text-muted small mt-2 mb-1"><strong>Request body</strong></div>
<pre style="font-size:0.75rem; max-height:200px; overflow:auto; margin:0; background:var(--pico-code-background-color); padding:0.5rem; border-radius:4px;">{{ log.request_body }}</pre>
{% endif %}
</div>
<div>
<div class="text-muted small mb-1"><strong>Response ({{ log.response_status }})</strong></div>
{% if log.response_body %}
<pre style="font-size:0.75rem; max-height:200px; overflow:auto; margin:0; background:var(--pico-code-background-color); padding:0.5rem; border-radius:4px;">{{ log.response_body }}</pre>
{% else %}
<span class="text-muted"></span>
{% endif %}
</div>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{# ── pagination ── #}
{% if total_pages > 1 %}
<div style="display:flex; justify-content:center; gap:0.5rem; padding:1rem;">
{% if page > 1 %}
<a href="?service={{ filter_service }}&method={{ filter_method }}&status={{ filter_status }}&q={{ filter_q }}&hours={{ filter_hours }}&page={{ page - 1 }}" role="button" class="outline secondary sm">← Назад</a>
{% endif %}
<span class="text-muted" style="line-height:2.2rem;">Стр. {{ page }} / {{ total_pages }}</span>
{% if page < total_pages %}
<a href="?service={{ filter_service }}&method={{ filter_method }}&status={{ filter_status }}&q={{ filter_q }}&hours={{ filter_hours }}&page={{ page + 1 }}" role="button" class="outline secondary sm">Вперёд →</a>
{% endif %}
</div>
<span style="font-size:12px;color:#9EA8BE;">Стр. {{ page }} / {{ total_pages }}</span>
{% if page < total_pages %}
<a href="?service={{ filter_service }}&method={{ filter_method }}&status={{ filter_status }}&q={{ filter_q }}&hours={{ filter_hours }}&page={{ page + 1 }}" class="btn btn-outline btn-sm">Вперёд →</a>
{% endif %}
</div>
{% endif %}
{% else %}
<div class="text-center py-5 text-muted">
<i class="bi bi-journal-x" style="font-size:2rem;"></i>
<p class="mt-2">Записей не найдено за выбранный период.</p>
</div>
{% endif %}
</article>
<style>
.badge { display:inline-block; padding:0.1rem 0.4rem; border-radius:4px; font-size:0.75rem; font-weight:600; }
.badge-evotor { background:#e8f4fd; color:#0986E2; }
.badge-vk { background:#e8f0fe; color:#3b5998; }
.text-danger { color:#dc3545; }
</style>
{% else %}
<div class="empty-state">
<i class="bi bi-journal-x"></i>
<p>Записей не найдено за выбранный период.</p>
</div>
{% endif %}
</div>
{% endblock %}
{% block scripts %}
<script>
function toggleDetail(id) {
const row = document.getElementById('detail-' + id);
row.style.display = row.style.display === 'none' ? 'table-row' : 'none';
const row = document.getElementById('detail-' + id);
row.style.display = row.style.display === 'none' ? 'table-row' : 'none';
}
</script>
{% endblock %}

View File

@@ -1,40 +1,44 @@
{% extends "base.html" %}
{% block title %}Роли и права — ЭВОСИНК{% endblock %}
{% block title %}Роли и права — Мои Товары{% endblock %}
{% block page_title %}Роли и права{% endblock %}
{% block content %}
<nav class="breadcrumb">
<li class="breadcrumb-item"><a href="/admin/users">Пользователи</a></li>
<li class="breadcrumb-item active">Роли и права</li>
</nav>
<ol class="breadcrumb">
<li><a href="/admin/users">Пользователи</a></li>
<li>Роли и права</li>
</ol>
<h1 style="font-size:1.3rem;" class="mb-3"><i class="bi bi-shield-lock me-2"></i>Роли и права</h1>
<div class="pg-title">Роли и права</div>
<div class="pg-sub">Управление разрешениями для каждой роли</div>
{% for role in roles %}
<article class="card mb-3">
<header>
<h2 style="font-size:1rem;">{{ role.name }}
<span class="text-muted small fw-normal">— {{ role.description or '' }}</span>
</h2>
</header>
<div class="card-body">
<form method="post" action="/admin/roles/{{ role.id }}/permissions">
<div class="row gap-2 flex-wrap">
{% for perm in permissions %}
<div class="col-auto">
<label style="display:flex; align-items:center; gap:0.4rem; margin:0;">
<input type="checkbox" name="perm_{{ perm.id }}" value="{{ perm.id }}"
{% if perm.id in role_perm_ids[role.id] %}checked{% endif %}>
{{ perm.name }}
{% if perm.description %}
<span class="text-muted small">({{ perm.description }})</span>
{% endif %}
</label>
</div>
{% endfor %}
</div>
<button type="submit" class="sm mt-3">Сохранить права для «{{ role.name }}»</button>
</form>
<div class="card" style="margin-bottom:14px;">
<div class="card-hd">
<div>
<div class="card-title"><i class="bi bi-shield-lock" style="margin-right:6px;"></i>{{ role.name }}</div>
{% if role.description %}
<div class="card-sub">{{ role.description }}</div>
{% endif %}
</div>
</article>
</div>
<form method="post" action="/admin/roles/{{ role.id }}/permissions">
<div style="display:flex;flex-wrap:wrap;gap:12px;margin-bottom:16px;">
{% for perm in permissions %}
<label style="display:flex;align-items:center;gap:7px;cursor:pointer;font-size:13px;padding:6px 10px;border:1px solid #E4E6EE;border-radius:7px;background:#F9FAFB;">
<input type="checkbox" name="perm_{{ perm.id }}" value="{{ perm.id }}"
{% if perm.id in role_perm_ids[role.id] %}checked{% endif %}
style="accent-color:#FF5500;">
{{ perm.name }}
{% if perm.description %}
<span style="font-size:11px;color:#9EA8BE;">({{ perm.description }})</span>
{% endif %}
</label>
{% endfor %}
</div>
<button type="submit" class="btn btn-primary btn-sm">
<i class="bi bi-save"></i> Сохранить права для «{{ role.name }}»
</button>
</form>
</div>
{% endfor %}
{% endblock %}

View File

@@ -1,152 +1,176 @@
{% extends "base.html" %}
{% block title %}{{ target.first_name }} {{ target.last_name }} — Админ — ЭВОСИНК{% endblock %}
{% block title %}{{ target.first_name }} {{ target.last_name }} — Админ — Мои Товары{% endblock %}
{% block page_title %}Пользователь{% endblock %}
{% block content %}
<nav class="breadcrumb">
<li class="breadcrumb-item"><a href="/admin/users">Пользователи</a></li>
<li class="breadcrumb-item active">{{ target.first_name }} {{ target.last_name }}</li>
</nav>
<ol class="breadcrumb">
<li><a href="/admin/users">Пользователи</a></li>
<li>{{ target.first_name }} {{ target.last_name }}</li>
</ol>
{% if request.query_params.get('success') == 'reset_sent' %}
<div class="alert alert-success mb-3"><p>Ссылка для сброса пароля отправлена.</p></div>
<div class="alert alert-gr"><span><i class="bi bi-check-circle"></i></span><div>Ссылка для сброса пароля отправлена.</div></div>
{% elif request.query_params.get('success') == 'invite_sent' %}
<div class="alert alert-success mb-3"><p>Приглашение отправлено.</p></div>
<div class="alert alert-gr"><span><i class="bi bi-check-circle"></i></span><div>Приглашение отправлено.</div></div>
{% elif request.query_params.get('success') == 'saved' %}
<div class="alert alert-success mb-3"><p>Данные сохранены.</p></div>
<div class="alert alert-gr"><span><i class="bi bi-check-circle"></i></span><div>Данные сохранены.</div></div>
{% endif %}
<div class="row gap-3 align-start">
<div class="col-lg-6">
<article class="card">
<header><h2>Профиль</h2></header>
<ul class="list-group">
<li class="list-group-item"><span class="text-muted small">ID</span><span>{{ target.id }}</span></li>
<li class="list-group-item"><span class="text-muted small">Имя</span><span>{{ target.first_name }} {{ target.last_name }}</span></li>
<li class="list-group-item"><span class="text-muted small">Email</span>
<span>{{ target.email }}
{% if target.is_email_confirmed %}
<span class="badge badge-success ms-1">подтверждён</span>
{% else %}
<span class="badge badge-warning ms-1">не подтверждён</span>
{% endif %}
</span>
</li>
<li class="list-group-item"><span class="text-muted small">Телефон</span><span>{{ target.phone }}</span></li>
<li class="list-group-item"><span class="text-muted small">Роль</span>
<span>
{% if target.role == 'system' %}<span class="badge badge-danger">Системный</span>
{% elif target.role == 'admin' %}<span class="badge badge-warning">Администратор</span>
{% else %}<span class="badge badge-secondary">Пользователь</span>
{% endif %}
</span>
</li>
<li class="list-group-item"><span class="text-muted small">Статус</span>
<span>
{% if target.status == 'active' %}<span class="badge badge-success">Активен</span>
{% elif target.status == 'pending' %}<span class="badge badge-warning">Ожидает</span>
{% else %}<span class="badge badge-danger">Заблокирован</span>
{% endif %}
</span>
</li>
<li class="list-group-item"><span class="text-muted small">Регистрация</span><span>{{ target.created_at | datefmt }}</span></li>
{% if target.evotor_user_id %}
<li class="list-group-item"><span class="text-muted small">Эвотор ID</span><span class="font-monospace small">{{ target.evotor_user_id }}</span></li>
{% endif %}
{% if target.invite_token %}
<li class="list-group-item"><span class="text-muted small">Приглашение до</span><span>{{ target.invite_expires | datefmt }}</span></li>
{% endif %}
</ul>
</article>
<!-- User header -->
<div style="display:flex;align-items:center;gap:14px;margin-bottom:24px;">
<div class="avatar" style="width:48px;height:48px;font-size:16px;">
{{ target.first_name[0] if target.first_name else '?' }}{{ target.last_name[0] if target.last_name else '' }}
</div>
<div>
<div style="font-size:18px;font-weight:800;letter-spacing:-0.02em;">{{ target.first_name }} {{ target.last_name }}</div>
<div class="mono" style="font-size:12px;color:#9EA8BE;">{{ target.email }}</div>
</div>
</div>
{% if target.evotor_meta %}
<article class="card mt-3">
<header><h2>Данные Эвотор</h2></header>
<div class="card-body">
<pre class="font-monospace small" style="overflow-x:auto; white-space:pre-wrap; margin:0;">{{ target.evotor_meta | tojson(indent=2) }}</pre>
</div>
</article>
<div class="g2" style="align-items:start;">
<!-- Left column -->
<div style="display:flex;flex-direction:column;gap:14px;">
<!-- Profile -->
<div class="card">
<div class="card-hd"><div><div class="card-title">Профиль</div></div></div>
<div class="conn-detail">
<div class="conn-row"><span class="conn-k">ID</span><span class="conn-v">{{ target.id }}</span></div>
<div class="conn-row">
<span class="conn-k">Email</span>
<span class="conn-v" style="display:flex;align-items:center;gap:6px;">
{{ target.email }}
{% if target.is_email_confirmed %}
<span class="tag tag-gr" style="font-size:10px;padding:1px 6px;">подтверждён</span>
{% else %}
<span class="tag tag-yl" style="font-size:10px;padding:1px 6px;">не подтверждён</span>
{% endif %}
</span>
</div>
<div class="conn-row"><span class="conn-k">Телефон</span><span class="conn-v">{{ target.phone or '—' }}</span></div>
<div class="conn-row">
<span class="conn-k">Роль</span>
<span class="conn-v" style="font-family:inherit;">
{% if target.role == 'system' %}<span class="tag tag-rd" style="font-size:10.5px;">Системный</span>
{% elif target.role == 'admin' %}<span class="tag tag-or" style="font-size:10.5px;">Администратор</span>
{% else %}<span class="tag tag-dim" style="font-size:10.5px;">Пользователь</span>
{% endif %}
</span>
</div>
<div class="conn-row">
<span class="conn-k">Статус</span>
<span class="conn-v" style="font-family:inherit;">
{% if target.status == 'active' %}<span class="tag tag-gr"><span class="dot g"></span>Активен</span>
{% elif target.status == 'pending' %}<span class="tag tag-yl"><span class="dot y pulse"></span>Ожидает</span>
{% else %}<span class="tag tag-rd"><span class="dot r"></span>Заблокирован</span>
{% endif %}
</span>
</div>
<div class="conn-row"><span class="conn-k">Регистрация</span><span class="conn-v">{{ target.created_at | datefmt }}</span></div>
{% if target.evotor_user_id %}
<div class="conn-row"><span class="conn-k">Эвотор ID</span><span class="conn-v">{{ target.evotor_user_id }}</span></div>
{% endif %}
{% if target.invite_token %}
<div class="conn-row"><span class="conn-k">Приглашение до</span><span class="conn-v">{{ target.invite_expires | datefmt }}</span></div>
{% endif %}
</div>
</div>
<div class="col-lg-6">
<article class="card">
<header><h2>Действия</h2></header>
<div class="card-body d-grid gap-2">
{% if target.status != 'active' %}
<form method="post" action="/admin/users/{{ target.id }}/activate">
<button type="submit" class="w-100">
<i class="bi bi-check-circle me-1"></i>Активировать
</button>
</form>
{% endif %}
{% if target.status != 'suspended' %}
<form method="post" action="/admin/users/{{ target.id }}/suspend">
<button type="submit" class="w-100 outline danger">
<i class="bi bi-slash-circle me-1"></i>Заблокировать
</button>
</form>
{% endif %}
<form method="post" action="/admin/users/{{ target.id }}/reset-password">
<button type="submit" class="w-100 outline secondary">
<i class="bi bi-key me-1"></i>Сбросить пароль
</button>
</form>
<form method="post" action="/admin/users/{{ target.id }}/send-invite">
<button type="submit" class="w-100 outline secondary">
<i class="bi bi-envelope me-1"></i>Отправить приглашение
</button>
</form>
<form method="post" action="/admin/users/{{ target.id }}/view-as">
<button type="submit" class="w-100 outline">
<i class="bi bi-eye me-1"></i>Просмотр от имени пользователя
</button>
</form>
{% if user.role == 'system' and target.id != user.id %}
<form method="post" action="/admin/users/{{ target.id }}/delete"
onsubmit="return confirm('Удалить пользователя {{ target.email }}? Это действие необратимо.')">
<button type="submit" class="w-100 danger sm">
<i class="bi bi-trash me-1"></i>Удалить
</button>
</form>
{% endif %}
</div>
</article>
<article class="card mt-3">
<header><h2>Редактировать</h2></header>
<div class="card-body">
<form method="post" action="/admin/users/{{ target.id }}/edit">
<div class="row gap-2 mb-2">
<div class="col">
<label for="first_name">Имя
<input type="text" id="first_name" name="first_name" value="{{ target.first_name }}" required>
</label>
</div>
<div class="col">
<label for="last_name">Фамилия
<input type="text" id="last_name" name="last_name" value="{{ target.last_name }}" required>
</label>
</div>
</div>
<label for="email">Email
<input type="email" id="email" name="email" value="{{ target.email }}">
</label>
<label for="phone">Телефон
<input type="tel" id="phone" name="phone" value="{{ target.phone }}">
</label>
{% if user.role == 'system' %}
<label for="role">Роль
<select id="role" name="role">
<option value="user" {% if target.role == 'user' %}selected{% endif %}>Пользователь</option>
<option value="admin" {% if target.role == 'admin' %}selected{% endif %}>Администратор</option>
<option value="system" {% if target.role == 'system' %}selected{% endif %}>Системный</option>
</select>
</label>
{% endif %}
<button type="submit">Сохранить</button>
</form>
</div>
</article>
{% if target.evotor_meta %}
<div class="card">
<div class="card-hd"><div><div class="card-title">Данные Эвотор</div></div></div>
<pre>{{ target.evotor_meta | tojson(indent=2) }}</pre>
</div>
{% endif %}
</div>
<!-- Right column -->
<div style="display:flex;flex-direction:column;gap:14px;">
<!-- Actions -->
<div class="card">
<div class="card-title" style="margin-bottom:14px;">Действия</div>
<div style="display:flex;flex-direction:column;gap:8px;">
{% if target.status != 'active' %}
<form method="post" action="/admin/users/{{ target.id }}/activate">
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-check-circle"></i> Активировать
</button>
</form>
{% endif %}
{% if target.status != 'suspended' %}
<form method="post" action="/admin/users/{{ target.id }}/suspend">
<button type="submit" class="btn btn-danger w-100">
<i class="bi bi-slash-circle"></i> Заблокировать
</button>
</form>
{% endif %}
<form method="post" action="/admin/users/{{ target.id }}/reset-password">
<button type="submit" class="btn btn-outline w-100">
<i class="bi bi-key"></i> Сбросить пароль
</button>
</form>
<form method="post" action="/admin/users/{{ target.id }}/send-invite">
<button type="submit" class="btn btn-outline w-100">
<i class="bi bi-envelope"></i> Отправить приглашение
</button>
</form>
<form method="post" action="/admin/users/{{ target.id }}/view-as">
<button type="submit" class="btn btn-outline w-100">
<i class="bi bi-eye"></i> Просмотр от имени пользователя
</button>
</form>
{% if user.role == 'system' and target.id != user.id %}
<form method="post" action="/admin/users/{{ target.id }}/delete"
onsubmit="return confirm('Удалить пользователя {{ target.email }}? Это действие необратимо.')">
<button type="submit" class="btn btn-danger btn-sm w-100">
<i class="bi bi-trash"></i> Удалить
</button>
</form>
{% endif %}
</div>
</div>
<!-- Edit -->
<div class="card">
<div class="card-title" style="margin-bottom:14px;">Редактировать</div>
<form method="post" action="/admin/users/{{ target.id }}/edit">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
<div class="form-row">
<label class="form-lbl" for="first_name">Имя</label>
<input class="inp" type="text" id="first_name" name="first_name" value="{{ target.first_name }}" required>
</div>
<div class="form-row">
<label class="form-lbl" for="last_name">Фамилия</label>
<input class="inp" type="text" id="last_name" name="last_name" value="{{ target.last_name }}" required>
</div>
</div>
<div class="form-row">
<label class="form-lbl" for="email">Email</label>
<input class="inp" type="email" id="email" name="email" value="{{ target.email }}">
</div>
<div class="form-row">
<label class="form-lbl" for="phone">Телефон</label>
<input class="inp" type="tel" id="phone" name="phone" value="{{ target.phone }}">
</div>
{% if user.role == 'system' %}
<div class="form-row">
<label class="form-lbl" for="role">Роль</label>
<select class="inp" id="role" name="role">
<option value="user" {% if target.role == 'user' %}selected{% endif %}>Пользователь</option>
<option value="admin" {% if target.role == 'admin' %}selected{% endif %}>Администратор</option>
<option value="system" {% if target.role == 'system' %}selected{% endif %}>Системный</option>
</select>
</div>
{% endif %}
<button type="submit" class="btn btn-primary btn-sm">
<i class="bi bi-save"></i> Сохранить
</button>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,173 +1,201 @@
{% extends "base.html" %}
{% block title %}Пользователи — Администрирование — ЭВОСИНК{% endblock %}
{% block title %}Пользователи — Администрирование — Мои Товары{% endblock %}
{% block page_title %}Пользователи{% endblock %}
{% block content %}
<div class="d-flex justify-between align-center mb-3">
<h1 style="font-size:1.3rem; margin:0;"><i class="bi bi-people me-2"></i>Пользователи</h1>
<button onclick="document.getElementById('create-user-dialog').showModal()" class="sm">
<i class="bi bi-person-plus me-1"></i>Создать пользователя
</button>
<div class="pg-title">Пользователи</div>
<div class="pg-sub">Управление аккаунтами и подключениями пользователей</div>
<!-- Topbar action -->
<div style="display:flex;justify-content:flex-end;margin-bottom:16px;">
<button class="btn btn-primary btn-sm" onclick="document.getElementById('create-user-dialog').showModal()">
<i class="bi bi-person-plus"></i> Создать пользователя
</button>
</div>
<!-- Create user dialog -->
<dialog id="create-user-dialog">
<article>
<header>
<button aria-label="Закрыть" rel="prev" onclick="document.getElementById('create-user-dialog').close()"></button>
<h3>Создать пользователя</h3>
</header>
{% if create_errors %}
<div role="alert" class="alert alert-danger mb-3">
{% for e in create_errors %}<p>{{ e }}</p>{% endfor %}
<div class="dialog-hd">
<div class="dialog-title">Создать пользователя</div>
<button class="dialog-close" onclick="document.getElementById('create-user-dialog').close()">
<i class="bi bi-x-lg"></i>
</button>
</div>
<div class="dialog-body">
{% if create_errors %}
<div class="alert alert-rd" style="margin-bottom:14px;">
<span><i class="bi bi-x-circle"></i></span>
<div>{% for e in create_errors %}<div>{{ e }}</div>{% endfor %}</div>
</div>
{% endif %}
<form method="post" action="/admin/users/create" novalidate>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
<div class="form-row">
<label class="form-lbl" for="cu_first_name">Имя</label>
<input class="inp" type="text" id="cu_first_name" name="first_name"
value="{{ create_form.first_name if create_form else '' }}" required>
</div>
{% endif %}
<form method="post" action="/admin/users/create" novalidate>
<div class="row gap-2 mb-2">
<div class="col">
<label for="cu_first_name">Имя
<input type="text" id="cu_first_name" name="first_name" value="{{ create_form.first_name if create_form else '' }}" required>
</label>
</div>
<div class="col">
<label for="cu_last_name">Фамилия
<input type="text" id="cu_last_name" name="last_name" value="{{ create_form.last_name if create_form else '' }}">
</label>
</div>
</div>
<label for="cu_email">Email
<input type="text" id="cu_email" name="email" value="{{ create_form.email if create_form else '' }}" required>
</label>
<label for="cu_phone">Телефон
<input type="tel" id="cu_phone" name="phone" value="{{ create_form.phone if create_form else '' }}" placeholder="+7 (999) 999-99-99">
</label>
<label for="cu_password">Пароль
<input type="password" id="cu_password" name="password" required>
</label>
{% if user.role == 'system' %}
<label for="cu_role">Роль
<select id="cu_role" name="role">
<option value="user" {% if not create_form or create_form.role == 'user' %}selected{% endif %}>Пользователь</option>
<option value="admin" {% if create_form and create_form.role == 'admin' %}selected{% endif %}>Администратор</option>
</select>
</label>
{% endif %}
<footer class="d-flex gap-2 justify-end">
<button type="button" class="outline secondary" onclick="document.getElementById('create-user-dialog').close()">Отмена</button>
<button type="submit">Создать</button>
</footer>
</form>
</article>
<div class="form-row">
<label class="form-lbl" for="cu_last_name">Фамилия</label>
<input class="inp" type="text" id="cu_last_name" name="last_name"
value="{{ create_form.last_name if create_form else '' }}">
</div>
</div>
<div class="form-row">
<label class="form-lbl" for="cu_email">Email</label>
<input class="inp" type="text" id="cu_email" name="email"
value="{{ create_form.email if create_form else '' }}" required>
</div>
<div class="form-row">
<label class="form-lbl" for="cu_phone">Телефон</label>
<input class="inp" type="tel" id="cu_phone" name="phone"
value="{{ create_form.phone if create_form else '' }}" placeholder="+7 (999) 999-99-99">
</div>
<div class="form-row">
<label class="form-lbl" for="cu_password">Пароль</label>
<input class="inp" type="password" id="cu_password" name="password" required>
</div>
{% if user.role == 'system' %}
<div class="form-row">
<label class="form-lbl" for="cu_role">Роль</label>
<select class="inp" id="cu_role" name="role">
<option value="user" {% if not create_form or create_form.role == 'user' %}selected{% endif %}>Пользователь</option>
<option value="admin" {% if create_form and create_form.role == 'admin' %}selected{% endif %}>Администратор</option>
</select>
</div>
{% endif %}
<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:16px;">
<button type="button" class="btn btn-outline" onclick="document.getElementById('create-user-dialog').close()">Отмена</button>
<button type="submit" class="btn btn-primary">Создать</button>
</div>
</form>
</div>
</dialog>
{% if create_errors %}
<script>document.addEventListener('DOMContentLoaded', () => document.getElementById('create-user-dialog').showModal());</script>
{% endif %}
<article class="card mb-3">
<div class="card-body">
<form method="get" action="/admin/users" class="d-flex gap-2 flex-wrap align-center">
<input type="text" name="search" value="{{ search }}" placeholder="Поиск по имени, email, телефону" style="flex:1; min-width:200px; margin:0;">
<select name="status" style="width:auto; margin:0;">
<option value="">Все статусы</option>
<option value="pending" {% if status_filter == 'pending' %}selected{% endif %}>Ожидает</option>
<option value="active" {% if status_filter == 'active' %}selected{% endif %}>Активен</option>
<option value="suspended" {% if status_filter == 'suspended' %}selected{% endif %}>Заблокирован</option>
</select>
<select name="role" style="width:auto; margin:0;">
<option value="">Все роли</option>
<option value="user" {% if role_filter == 'user' %}selected{% endif %}>Пользователь</option>
<option value="admin" {% if role_filter == 'admin' %}selected{% endif %}>Администратор</option>
<option value="system" {% if role_filter == 'system' %}selected{% endif %}>Системный</option>
</select>
<button type="submit" class="sm">Найти</button>
{% if search or status_filter or role_filter %}
<a href="/admin/users" role="button" class="outline secondary sm">Сбросить</a>
{% endif %}
</form>
</div>
</article>
<article class="card">
<div class="table-scroll">
<table class="align-middle">
<thead>
<tr>
<th>ID</th>
<th>Имя</th>
<th>Email</th>
<th>Телефон</th>
<th>Роль</th>
<th>Статус</th>
<th>Эвотор</th>
<th>Дата</th>
<th></th>
</tr>
</thead>
<tbody>
{% for u in users %}
<tr>
<td class="text-muted small">{{ u.id }}</td>
<td>{{ u.first_name }} {{ u.last_name }}</td>
<td title="{{ u.email }}">
{{ u.email }}
{% if not u.is_email_confirmed %}
<span class="badge badge-warning ms-1" title="Email не подтверждён"><i class="bi bi-exclamation-circle"></i></span>
{% endif %}
</td>
<td>{{ u.phone or '—' }}</td>
<td>
{% if u.role == 'system' %}<span class="badge badge-danger">Системный</span>
{% elif u.role == 'admin' %}<span class="badge badge-warning">Админ</span>
{% else %}<span class="badge badge-secondary">Польз.</span>
{% endif %}
</td>
<td>
{% if u.status == 'active' %}<span class="badge badge-success">Активен</span>
{% elif u.status == 'pending' %}<span class="badge badge-warning">Ожидает</span>
{% else %}<span class="badge badge-danger">Заблок.</span>
{% endif %}
</td>
<td>
{% if u.evotor_user_id %}
<i class="bi bi-check-circle text-success" title="{{ u.evotor_user_id }}"></i>
{% else %}
<span class="text-muted"></span>
{% endif %}
</td>
<td class="text-muted small">{{ u.created_at | datefmt }}</td>
<td>
<a href="/admin/users/{{ u.id }}" role="button" class="outline sm">
<i class="bi bi-eye"></i>
</a>
</td>
</tr>
{% else %}
<tr>
<td colspan="9" class="text-center text-muted py-4">Пользователи не найдены</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if total_pages > 1 %}
<footer>
<div class="d-flex gap-2 justify-center align-center">
{% if page > 1 %}
<a href="?page={{ page - 1 }}&search={{ search }}&status={{ status_filter }}&role={{ role_filter }}" role="button" class="outline sm">«</a>
{% endif %}
<span class="text-muted small">Стр. {{ page }} из {{ total_pages }}</span>
{% if page < total_pages %}
<a href="?page={{ page + 1 }}&search={{ search }}&status={{ status_filter }}&role={{ role_filter }}" role="button" class="outline sm">»</a>
{% endif %}
</div>
</footer>
<!-- Search / filter bar -->
<div class="card" style="margin-bottom:14px;padding:14px 20px;">
<form method="get" action="/admin/users" style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
<input class="inp" type="text" name="search" value="{{ search }}"
placeholder="Поиск по имени, email, телефону" style="flex:1;min-width:200px;">
<select class="inp" name="status" style="width:auto;">
<option value="">Все статусы</option>
<option value="pending" {% if status_filter == 'pending' %}selected{% endif %}>Ожидает</option>
<option value="active" {% if status_filter == 'active' %}selected{% endif %}>Активен</option>
<option value="suspended" {% if status_filter == 'suspended' %}selected{% endif %}>Заблокирован</option>
</select>
<select class="inp" name="role" style="width:auto;">
<option value="">Все роли</option>
<option value="user" {% if role_filter == 'user' %}selected{% endif %}>Пользователь</option>
<option value="admin" {% if role_filter == 'admin' %}selected{% endif %}>Администратор</option>
<option value="system" {% if role_filter == 'system' %}selected{% endif %}>Системный</option>
</select>
<button type="submit" class="btn btn-primary btn-sm">Найти</button>
{% if search or status_filter or role_filter %}
<a href="/admin/users" class="btn btn-outline btn-sm">Сбросить</a>
{% endif %}
</article>
</form>
</div>
<!-- Users table -->
<div class="card" style="padding:0;">
<div class="table-wrap">
<table class="tbl">
<thead>
<tr>
<th>ID</th>
<th>Пользователь</th>
<th>Телефон</th>
<th>Роль</th>
<th>Статус</th>
<th>Эвотор</th>
<th>Дата</th>
<th></th>
</tr>
</thead>
<tbody>
{% for u in users %}
<tr>
<td><span class="mono" style="font-size:11px;color:#9EA8BE;">{{ u.id }}</span></td>
<td>
<div style="display:flex;align-items:center;gap:10px;">
<div class="avatar" style="width:30px;height:30px;font-size:10px;">
{{ u.first_name[0] if u.first_name else '?' }}{{ u.last_name[0] if u.last_name else '' }}
</div>
<div>
<div class="tbl-name">{{ u.first_name }} {{ u.last_name }}</div>
<div class="tbl-sub">
{{ u.email }}
{% if not u.is_email_confirmed %}
<span class="tag tag-yl" style="font-size:9.5px;padding:0 5px;margin-left:4px;"><i class="bi bi-exclamation-circle"></i></span>
{% endif %}
</div>
</div>
</div>
</td>
<td style="font-size:12px;color:#9EA8BE;">{{ u.phone or '—' }}</td>
<td>
{% if u.role == 'system' %}
<span class="tag tag-rd" style="font-size:10.5px;">Системный</span>
{% elif u.role == 'admin' %}
<span class="tag tag-or" style="font-size:10.5px;">Админ</span>
{% else %}
<span class="tag tag-dim" style="font-size:10.5px;">Польз.</span>
{% endif %}
</td>
<td>
{% if u.status == 'active' %}
<span class="tag tag-gr"><span class="dot g"></span>Активен</span>
{% elif u.status == 'pending' %}
<span class="tag tag-yl"><span class="dot y pulse"></span>Ожидает</span>
{% else %}
<span class="tag tag-rd"><span class="dot r"></span>Заблок.</span>
{% endif %}
</td>
<td>
{% if u.evotor_user_id %}
<span class="tag tag-gr" style="font-size:10.5px;"><i class="bi bi-check-circle"></i></span>
{% else %}
<span style="color:#9EA8BE;font-size:12px;"></span>
{% endif %}
</td>
<td><span class="mono" style="font-size:11px;color:#9EA8BE;">{{ u.created_at | datefmt }}</span></td>
<td>
<a href="/admin/users/{{ u.id }}" class="btn btn-outline btn-xs">
<i class="bi bi-eye"></i>
</a>
</td>
</tr>
{% else %}
<tr>
<td colspan="8" class="text-center" style="padding:32px;color:#9EA8BE;">Пользователи не найдены</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if total_pages > 1 %}
<div class="pagination">
{% if page > 1 %}
<a href="?page={{ page - 1 }}&search={{ search }}&status={{ status_filter }}&role={{ role_filter }}" class="btn btn-outline btn-sm">« Назад</a>
{% endif %}
<span style="font-size:12px;color:#9EA8BE;">Стр. {{ page }} из {{ total_pages }}</span>
{% if page < total_pages %}
<a href="?page={{ page + 1 }}&search={{ search }}&status={{ status_filter }}&role={{ role_filter }}" class="btn btn-outline btn-sm">Вперёд »</a>
{% endif %}
</div>
{% endif %}
</div>
{% if user.role == 'system' %}
<div class="mt-3 text-end">
<a href="/admin/roles" role="button" class="outline secondary sm">
<i class="bi bi-shield-lock me-1"></i>Управление ролями
</a>
<div style="margin-top:14px;text-align:right;">
<a href="/admin/roles" class="btn btn-outline btn-sm">
<i class="bi bi-shield-lock"></i> Управление ролями
</a>
</div>
{% endif %}
{% endblock %}