Update: admin panel, SMTP/EWS/Outlook mail modes, QR fix, event folders, security improvements
This commit is contained in:
@@ -58,6 +58,7 @@
|
||||
<h5 class="card-title">Действия</h5>
|
||||
<a href="{{ url_for('main.admin_report') }}" class="btn btn-success">Выгрузить отчёт Excel</a>
|
||||
<form action="{{ url_for('main.admin_send_invitations') }}" method="POST" class="d-inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-outline-primary">Разослать приглашения</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -77,7 +78,7 @@
|
||||
<tbody>
|
||||
{% for check_in in recent_check_ins %}
|
||||
<tr>
|
||||
<td>{{ check_in.checked_in_at.strftime('%d.%m.%Y %H:%M:%S') }}</td>
|
||||
<td>{{ check_in.checked_in_at | localtime('%d.%m.%Y %H:%M:%S') }}</td>
|
||||
<td>{{ check_in.guest.full_name }}</td>
|
||||
<td>{{ check_in.guest.department }}</td>
|
||||
</tr>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Рассылка приглашений — InviteMe{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h4 class="card-title mb-0">Рассылка приглашений</h4>
|
||||
<a href="{{ url_for('main.admin_email_settings') }}" class="btn btn-outline-secondary btn-sm">Настройки почты</a>
|
||||
</div>
|
||||
|
||||
{% if event_counts %}
|
||||
<form method="POST" action="{{ url_for('main.admin_email_send') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="event_id" class="form-label">Мероприятие</label>
|
||||
<select class="form-select" id="event_id" name="event_id" required>
|
||||
<option value="">Выберите мероприятие</option>
|
||||
{% for item in event_counts %}
|
||||
<option value="{{ item.event.id }}">
|
||||
{{ item.event.name }} — неотправлено: {{ item.unsent }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
Перед рассылкой убедитесь, что в <a href="{{ url_for('main.admin_email_settings') }}">настройках почты</a> указаны корректные SMTP-данные.
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary" onclick="return confirm('Начать рассылку приглашений?')">
|
||||
Разослать приглашения
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="alert alert-warning">У вас пока нет мероприятий для рассылки.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,127 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Настройки почты — InviteMe{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<h4 class="card-title mb-4">Настройки почты для рассылки приглашений</h4>
|
||||
<form method="POST" action="{{ url_for('main.admin_email_settings') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label d-block">Способ отправки</label>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="mail_mode" id="mode_smtp" value="smtp" {% if settings.mail_mode == 'smtp' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="mode_smtp">SMTP</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="mail_mode" id="mode_ews" value="ews" {% if settings.mail_mode == 'ews' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="mode_ews">EWS (Exchange)</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="mail_mode" id="mode_outlook" value="outlook" {% if settings.mail_mode == 'outlook' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="mode_outlook">Локальный Outlook</label>
|
||||
</div>
|
||||
<div class="form-text text-muted">
|
||||
SMTP/EWS — для серверной установки. Outlook — только если InviteMe запущен на ПК с Outlook.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="common-fields">
|
||||
<div class="mb-3">
|
||||
<label for="mail_username" class="form-label">Логин / Email</label>
|
||||
<input type="text" class="form-control" id="mail_username" name="mail_username" value="{{ settings.mail_username }}">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="mail_password" class="form-label">Пароль</label>
|
||||
<input type="password" class="form-control" id="mail_password" name="mail_password" placeholder="Оставьте пустым, чтобы не менять">
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="mail_default_sender" class="form-label">Имя отправителя (необязательно)</label>
|
||||
<input type="text" class="form-control" id="mail_default_sender" name="mail_default_sender" value="{{ settings.mail_default_sender or '' }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="smtp-fields" {% if settings.mail_mode != 'smtp' %}style="display:none;"{% endif %}>
|
||||
<div class="row">
|
||||
<div class="col-md-8 mb-3">
|
||||
<label for="mail_server" class="form-label">SMTP-сервер</label>
|
||||
<input type="text" class="form-control" id="mail_server" name="mail_server" value="{{ settings.mail_server }}">
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="mail_port" class="form-label">Порт</label>
|
||||
<input type="number" class="form-control" id="mail_port" name="mail_port" value="{{ settings.mail_port }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="mail_use_ssl" name="mail_use_ssl" value="1" {% if settings.mail_use_ssl %}checked{% endif %}>
|
||||
<label class="form-check-label" for="mail_use_ssl">Использовать SSL</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="ews-fields" {% if settings.mail_mode != 'ews' %}style="display:none;"{% endif %}>
|
||||
<div class="mb-4">
|
||||
<label for="ews_url" class="form-label">EWS URL</label>
|
||||
<input type="text" class="form-control" id="ews_url" name="ews_url" value="{{ settings.ews_url or '' }}" placeholder="https://mail.company.com/EWS/Exchange.asmx">
|
||||
<div class="form-text text-muted">URL вида <code>https://mail.integrita.ru/EWS/Exchange.asmx</code></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="outlook-info" {% if settings.mail_mode != 'outlook' %}style="display:none;"{% endif %} class="alert alert-info">
|
||||
Outlook должен быть установлен и запущен на сервере, где работает приложение. Браузер не может запустить Outlook на твоём компьютере.
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">Сохранить настройки</button>
|
||||
<a href="{{ url_for('main.admin_email_send') }}" class="btn btn-outline-primary">Перейти к рассылке</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title mb-3">Отправить тестовое письмо</h5>
|
||||
<form method="POST" action="{{ url_for('main.admin_email_test') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="input-group">
|
||||
<input type="email" class="form-control" name="test_address" placeholder="address@example.com" required>
|
||||
<button class="btn btn-success" type="submit">Отправить тест</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
const radios = document.querySelectorAll('input[name="mail_mode"]');
|
||||
const smtpFields = document.getElementById('smtp-fields');
|
||||
const ewsFields = document.getElementById('ews-fields');
|
||||
const outlookInfo = document.getElementById('outlook-info');
|
||||
const commonInputs = document.querySelectorAll('#common-fields input, #smtp-fields input, #ews-fields input');
|
||||
|
||||
function toggleFields() {
|
||||
const mode = document.querySelector('input[name="mail_mode"]:checked').value;
|
||||
smtpFields.style.display = mode === 'smtp' ? 'block' : 'none';
|
||||
ewsFields.style.display = mode === 'ews' ? 'block' : 'none';
|
||||
outlookInfo.style.display = mode === 'outlook' ? 'block' : 'none';
|
||||
|
||||
// disable hidden inputs to avoid sending stale data
|
||||
document.querySelectorAll('#smtp-fields input, #ews-fields input').forEach(i => {
|
||||
i.disabled = (mode === 'outlook') || (i.closest('#smtp-fields') && mode !== 'smtp') || (i.closest('#ews-fields') && mode !== 'ews');
|
||||
});
|
||||
document.querySelectorAll('#common-fields input').forEach(i => i.disabled = mode === 'outlook');
|
||||
}
|
||||
|
||||
radios.forEach(r => r.addEventListener('change', toggleFields));
|
||||
toggleFields();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -8,6 +8,7 @@
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Название *</label>
|
||||
<input type="text" class="form-control" id="name" name="name" value="{{ event.name if event else '' }}" required>
|
||||
@@ -23,7 +24,7 @@
|
||||
<div class="mb-3">
|
||||
<label for="event_date" class="form-label">Дата и время</label>
|
||||
<input type="datetime-local" class="form-control" id="event_date" name="event_date"
|
||||
value="{{ event.event_date.strftime('%Y-%m-%dT%H:%M') if event and event.event_date else '' }}">
|
||||
value="{{ event.event_date | localtime('%Y-%m-%dT%H:%M') if event and event.event_date else '' }}">
|
||||
</div>
|
||||
{% if event %}
|
||||
<div class="mb-3 form-check">
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<h5 class="card-title">{{ event.name }}</h5>
|
||||
<p class="card-text text-muted">
|
||||
{% if event.event_date %}
|
||||
{{ event.event_date.strftime('%d.%m.%Y %H:%M') }}<br>
|
||||
{{ event.event_date | localtime('%d.%m.%Y %H:%M') }}<br>
|
||||
{% endif %}
|
||||
{% if event.location %}
|
||||
{{ event.location }}<br>
|
||||
@@ -28,20 +28,54 @@
|
||||
{% endif %}
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<form action="{{ url_for('main.admin_event_switch', event_id=event.id) }}" method="POST" class="d-inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-success btn-sm">Выбрать</button>
|
||||
</form>
|
||||
<a href="{{ url_for('main.admin_event_edit', event_id=event.id) }}" class="btn btn-outline-primary btn-sm">Изменить</a>
|
||||
<a href="{{ url_for('main.admin_event_ticket', event_id=event.id) }}" class="btn btn-outline-info btn-sm">Билет</a>
|
||||
{% if current_user and current_user.is_admin %}
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" data-bs-toggle="modal" data-bs-target="#deleteEventModal{{ event.id }}">Удалить</button>
|
||||
{% endif %}
|
||||
{% if not event.is_active %}
|
||||
<span class="badge bg-secondary">Неактивно</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer text-muted">
|
||||
Создано: {{ event.created_at.strftime('%d.%m.%Y') }}
|
||||
Создано: {{ event.created_at | localdate('%d.%m.%Y') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if current_user and current_user.is_admin %}
|
||||
<!-- Modal for hard delete -->
|
||||
<div class="modal fade" id="deleteEventModal{{ event.id }}" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form action="{{ url_for('main.admin_event_delete_hard', event_id=event.id) }}" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Удаление мероприятия</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Вы собираетесь полностью удалить мероприятие <strong>{{ event.name }}</strong>.</p>
|
||||
<p class="text-danger">Все гости, QR-коды, билеты и логотипы этого мероприятия будут удалены безвозвратно.</p>
|
||||
<div class="mb-3">
|
||||
<label for="deleteReason{{ event.id }}" class="form-label">Причина удаления <span class="text-danger">*</span></label>
|
||||
<textarea class="form-control" id="deleteReason{{ event.id }}" name="reason" rows="3" required></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
|
||||
<button type="submit" class="btn btn-danger">Удалить</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info">Пока нет ни одного мероприятия. <a href="{{ url_for('main.admin_event_add') }}">Создайте первое</a>.</div>
|
||||
|
||||
@@ -8,7 +8,14 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
{% if guest %}
|
||||
<div class="mb-3">
|
||||
<a href="{{ url_for('main.admin_guest_ticket', guest_id=guest.id) }}" target="_blank" class="btn btn-outline-info">Посмотреть билет</a>
|
||||
<a href="{{ url_for('main.admin_guest_qr', guest_id=guest.id) }}" target="_blank" class="btn btn-outline-success">QR-код</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<form method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="last_name" class="form-label">Фамилия</label>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
<form method="GET" class="row g-3 mb-4">
|
||||
<div class="col-auto">
|
||||
<input type="text" name="q" class="form-control" placeholder="Поиск по ФИО" value="{{ search }}">
|
||||
<input type="text" name="q" class="form-control" placeholder="Поиск по ФИО, отделу, email..." value="{{ search }}">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button type="submit" class="btn btn-outline-primary">Найти</button>
|
||||
@@ -40,17 +40,17 @@
|
||||
<td>{{ guest.email }}</td>
|
||||
<td>{{ guest.phone or '-' }}</td>
|
||||
<td>
|
||||
{% if guest.photo_filename %}
|
||||
<span class="badge bg-success">Есть</span>
|
||||
{% if guest.has_photo_file %}
|
||||
<a href="{{ guest.photo_url }}" target="_blank" class="badge bg-success text-decoration-none">Есть</a>
|
||||
{% else %}
|
||||
<span class="badge bg-warning text-dark">Нет</span>
|
||||
<span class="badge bg-warning text-dark" title="{% if guest.photo_filename %}Имя файла указано, но файл не загружен{% else %}Не указано{% endif %}">Нет</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if guest.qr_filename %}
|
||||
<span class="badge bg-success">Да</span>
|
||||
<a href="{{ url_for('main.admin_guest_qr', guest_id=guest.id) }}" target="_blank" class="badge bg-success text-decoration-none">Да</a>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Нет</span>
|
||||
<a href="{{ url_for('main.admin_guest_qr', guest_id=guest.id) }}" target="_blank" class="badge bg-secondary text-decoration-none">Нет</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
@@ -62,10 +62,14 @@
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ url_for('main.admin_guest_edit', guest_id=guest.id) }}" class="btn btn-sm btn-outline-primary">Изменить</a>
|
||||
<form action="{{ url_for('main.admin_guest_resend', guest_id=guest.id) }}" method="POST" class="d-inline">
|
||||
<button type="submit" class="btn btn-sm btn-outline-success">QR</button>
|
||||
<a href="{{ url_for('main.admin_guest_ticket', guest_id=guest.id) }}" target="_blank" class="btn btn-sm btn-outline-info">Билет</a>
|
||||
<a href="{{ url_for('main.admin_guest_qr', guest_id=guest.id) }}" target="_blank" class="btn btn-sm btn-outline-success">QR</a>
|
||||
<form action="{{ url_for('main.admin_guest_resend', guest_id=guest.id) }}" method="POST" class="d-inline" onsubmit="return confirm('Отправить приглашение повторно?')">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-warning">Пригласить</button>
|
||||
</form>
|
||||
<form action="{{ url_for('main.admin_guest_delete', guest_id=guest.id) }}" method="POST" class="d-inline" onsubmit="return confirm('Заблокировать гостя?')">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">Блок</button>
|
||||
</form>
|
||||
</td>
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Аудит-лог — InviteMe{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1>Аудит-лог</h1>
|
||||
</div>
|
||||
|
||||
<form method="GET" class="row g-3 mb-4">
|
||||
<div class="col-md-3">
|
||||
<select name="action" class="form-select">
|
||||
<option value="">Все действия</option>
|
||||
{% for action in actions %}
|
||||
<option value="{{ action }}" {% if action == action_filter %}selected{% endif %}>{{ action }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<input type="text" name="user" class="form-control" placeholder="Пользователь" value="{{ user_filter }}">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<button type="submit" class="btn btn-primary">Фильтр</button>
|
||||
<a href="{{ url_for('main.admin_logs') }}" class="btn btn-secondary">Сброс</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Время</th>
|
||||
<th>Пользователь</th>
|
||||
<th>IP</th>
|
||||
<th>Действие</th>
|
||||
<th>Объект</th>
|
||||
<th>ID объекта</th>
|
||||
<th>Детали</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for log in logs.items %}
|
||||
<tr>
|
||||
<td>{{ log.created_at | localtime('%d.%m.%Y %H:%M:%S') }}</td>
|
||||
<td>{{ log.username or '-' }}</td>
|
||||
<td>{{ log.ip_address or '-' }}</td>
|
||||
<td><span class="badge bg-secondary">{{ log.action }}</span></td>
|
||||
<td>{{ log.target_type or '-' }}</td>
|
||||
<td>{{ log.target_id or '-' }}</td>
|
||||
<td>{{ log.details or '-' }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="7" class="text-center text-muted">Записи не найдены</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if logs.pages > 1 %}
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% if logs.has_prev %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{{ url_for('main.admin_logs', page=logs.prev_num, action=action_filter, user=user_filter) }}">Назад</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% for page_num in logs.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
|
||||
{% if page_num %}
|
||||
{% if page_num == logs.page %}
|
||||
<li class="page-item active"><span class="page-link">{{ page_num }}</span></li>
|
||||
{% else %}
|
||||
<li class="page-item"><a class="page-link" href="{{ url_for('main.admin_logs', page=page_num, action=action_filter, user=user_filter) }}">{{ page_num }}</a></li>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<li class="page-item disabled"><span class="page-link">...</span></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if logs.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{{ url_for('main.admin_logs', page=logs.next_num, action=action_filter, user=user_filter) }}">Вперёд</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,33 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Админ-панель — InviteMe{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<h2>Админ-панель</h2>
|
||||
<p class="text-muted">Все разделы управления в одном месте</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
|
||||
{% for card in cards %}
|
||||
<div class="col">
|
||||
<div class="card h-100 {% if card.disabled %}opacity-75{% endif %}">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<span style="font-size: 2rem; margin-right: 0.75rem;">{{ card.icon }}</span>
|
||||
<h5 class="card-title mb-0">{{ card.title }}</h5>
|
||||
</div>
|
||||
<p class="card-text text-muted">{{ card.description }}</p>
|
||||
{% if card.disabled %}
|
||||
<a href="{{ card.url }}" class="btn btn-outline-secondary disabled">Сначала выберите мероприятие</a>
|
||||
{% else %}
|
||||
<a href="{{ card.url }}" class="btn btn-primary">{{ card.btn }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -14,12 +14,13 @@
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Настройки дизайна</h5>
|
||||
<form method="POST" id="ticket-form" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="mb-3">
|
||||
<label for="logo_file" class="form-label">Логотип компании</label>
|
||||
<input type="file" class="form-control" id="logo_file" name="logo_file" accept="image/*">
|
||||
{% if event.logo_filename %}
|
||||
<div class="mt-2 d-flex align-items-center gap-2">
|
||||
<img src="{{ url_for('static', filename='uploads/logos/' + event.logo_filename) }}" alt="Логотип" style="max-height: 60px; max-width: 200px;">
|
||||
<img src="{{ url_for('static', filename='uploads/logos/event_' + event.id|string + '/' + event.logo_filename) }}" alt="Логотип" style="max-height: 60px; max-width: 200px;">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="delete_logo" name="delete_logo" value="1">
|
||||
<label class="form-check-label" for="delete_logo">Удалить</label>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<h5 class="card-title">1. Загрузить Excel со списком гостей</h5>
|
||||
<p class="text-muted">Столбцы: Отдел, Фамилия, Имя, Отчество, Должность, Почта, Телефон, Название фото</p>
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="mb-3">
|
||||
<label for="excel_file" class="form-label">Excel-файл</label>
|
||||
<input type="file" class="form-control" id="excel_file" name="excel_file" accept=".xlsx,.xls" required>
|
||||
@@ -33,6 +34,7 @@
|
||||
<h5 class="card-title">2. Загрузить фото гостей</h5>
|
||||
<p class="text-muted">Фото должны называться точно так же, как указано в столбце «Название фото»</p>
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="mb-3">
|
||||
<label for="photo_files" class="form-label">Фото (можно несколько)</label>
|
||||
<input type="file" class="form-control" id="photo_files" name="photo_files" accept="image/*" multiple>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Логин *</label>
|
||||
<input type="text" class="form-control" id="username" name="username" value="{{ user.username if user else '' }}" required>
|
||||
|
||||
@@ -38,11 +38,12 @@
|
||||
<span class="badge bg-secondary">Заблокирован</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ user.created_at.strftime('%d.%m.%Y %H:%M') }}</td>
|
||||
<td>{{ user.created_at | localtime('%d.%m.%Y %H:%M') }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('main.admin_user_edit', user_id=user.id) }}" class="btn btn-sm btn-outline-primary">Изменить</a>
|
||||
{% if current_user.id != user.id %}
|
||||
<form action="{{ url_for('main.admin_user_delete', user_id=user.id) }}" method="POST" class="d-inline" onsubmit="return confirm('Заблокировать пользователя?')">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">Блок</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
+8
-22
@@ -6,38 +6,23 @@
|
||||
<title>{% block title %}InviteMe{% endblock %}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||
<link rel="icon" type="image/png" href="{{ url_for('static', filename='images/inviteme_logo.png') }}">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="{{ url_for('main.index') }}">InviteMe</a>
|
||||
<a class="navbar-brand d-flex align-items-center" href="{{ url_for('main.index') }}">
|
||||
<img src="{{ url_for('static', filename='images/inviteme_logo.png') }}" alt="InviteMe" class="app-logo">
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
{% if current_user and current_user.role in ('admin', 'organizer') %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('main.admin_events') }}">Мероприятия</a>
|
||||
</li>
|
||||
{% if current_event %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('main.admin_dashboard') }}">Аналитика</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('main.admin_upload') }}">Загрузка</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('main.admin_guests') }}">Гости</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if current_user and current_user.is_admin %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('main.admin_users') }}">Пользователи</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if current_user and current_user.role in ('admin', 'organizer', 'staff') %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('main.admin_panel') }}">Админ-панель</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('main.check_in') }}">Проверка приглашения</a>
|
||||
</li>
|
||||
@@ -55,6 +40,7 @@
|
||||
{% for event in available_events %}
|
||||
<li>
|
||||
<form action="{{ url_for('main.admin_event_switch', event_id=event.id) }}" method="POST" class="d-inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="dropdown-item {% if current_event and current_event.id == event.id %}active{% endif %}">
|
||||
{{ event.name }}
|
||||
</button>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Выберите мероприятие</h5>
|
||||
<form method="POST" action="{{ url_for('main.check_in', token=token) }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="mb-3">
|
||||
<select id="event-select" name="event_id" class="form-select" onchange="this.form.submit()">
|
||||
<option value="">-- Мероприятие --</option>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Тестовое письмо InviteMe</title>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background-color:#0f172a;font-family:'Segoe UI',Roboto,Arial,sans-serif;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:#0f172a;padding:40px 0;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table width="600" cellpadding="0" cellspacing="0" border="0" style="background:rgba(30,41,59,0.95);border:1px solid rgba(148,163,184,0.15);border-radius:16px;overflow:hidden;">
|
||||
<tr>
|
||||
<td style="padding:30px;text-align:center;">
|
||||
<h2 style="color:#22d3ee;margin:0 0 16px 0;">InviteMe</h2>
|
||||
<p style="color:#e2e8f0;font-size:16px;line-height:1.6;margin:0;">
|
||||
Это тестовое письмо. Если вы его видите, SMTP-настройки работают корректно.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:20px 30px;background:rgba(15,23,42,0.6);text-align:center;">
|
||||
<p style="color:#94a3b8;font-size:13px;margin:0;">
|
||||
Отправлено из панели управления InviteMe
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -7,9 +7,13 @@
|
||||
<div class="col-md-4">
|
||||
<div class="card mt-5">
|
||||
<div class="card-body p-4">
|
||||
<div class="text-center mb-3">
|
||||
<img src="{{ url_for('static', filename='images/inviteme_logo.png') }}" alt="InviteMe" class="login-logo">
|
||||
</div>
|
||||
<h3 class="card-title text-center mb-4" style="color: var(--accent-color); text-shadow: 0 0 12px var(--accent-glow);">InviteMe</h3>
|
||||
<p class="text-muted text-center mb-4">Система регистрации гостей</p>
|
||||
<form method="POST" action="{{ url_for('main.login') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Логин</label>
|
||||
<input type="text" class="form-control" id="username" name="username" required autofocus>
|
||||
|
||||
Reference in New Issue
Block a user