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:
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
Reference in New Issue
Block a user