Initial commit: InviteMe event registration app

This commit is contained in:
InviteMe Developer
2026-08-28 12:28:52 +03:00
commit b4e10b8448
23 changed files with 1718 additions and 0 deletions
+91
View File
@@ -0,0 +1,91 @@
{% extends "base.html" %}
{% block title %}Аналитика — InviteMe{% endblock %}
{% block content %}
<h1 class="mb-4">Аналитика мероприятия</h1>
<div class="row">
<div class="col-md-3">
<div class="card text-white bg-info mb-3">
<div class="card-body">
<h5 class="card-title">Всего гостей</h5>
<p class="card-text display-6">{{ total }}</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-white bg-success mb-3">
<div class="card-body">
<h5 class="card-title">Пришло</h5>
<p class="card-text display-6">{{ checked_in }}</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-white bg-secondary mb-3">
<div class="card-body">
<h5 class="card-title">Осталось</h5>
<p class="card-text display-6">{{ not_checked_in }}</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-white bg-primary mb-3">
<div class="card-body">
<h5 class="card-title">Посещаемость</h5>
<p class="card-text display-6">{{ attendance_percent }}%</p>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-body">
<h5 class="card-title">Быстрый поиск</h5>
<form action="{{ url_for('main.admin_guests') }}" method="GET" class="d-flex">
<input type="text" name="q" class="form-control me-2" placeholder="Фамилия или ФИО" required>
<button type="submit" class="btn btn-primary">Найти</button>
</form>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-body">
<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">
<button type="submit" class="btn btn-outline-primary">Разослать приглашения</button>
</form>
</div>
</div>
</div>
</div>
<h3 class="mt-5">Последние входы</h3>
<table class="table table-striped">
<thead>
<tr>
<th>Время</th>
<th>ФИО</th>
<th>Отдел</th>
</tr>
</thead>
<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.guest.full_name }}</td>
<td>{{ check_in.guest.department }}</td>
</tr>
{% else %}
<tr>
<td colspan="3" class="text-muted">Пока никто не отмечен</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
+46
View File
@@ -0,0 +1,46 @@
{% extends "base.html" %}
{% block title %}{{ 'Редактировать' if guest else 'Добавить' }} гостя — InviteMe{% endblock %}
{% block content %}
<h1 class="mb-4">{{ 'Редактировать' if guest else 'Добавить' }} гостя</h1>
<div class="row">
<div class="col-md-6">
<form method="POST">
<div class="mb-3">
<label for="full_name" class="form-label">ФИО</label>
<input type="text" class="form-control" id="full_name" name="full_name" value="{{ guest.full_name if guest else '' }}" required>
</div>
<div class="mb-3">
<label for="department" class="form-label">Отдел</label>
<input type="text" class="form-control" id="department" name="department" value="{{ guest.department if guest else '' }}" required>
</div>
<div class="mb-3">
<label for="position" class="form-label">Должность</label>
<input type="text" class="form-control" id="position" name="position" value="{{ guest.position if guest else '' }}">
</div>
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email" value="{{ guest.email if guest else '' }}" required>
</div>
<div class="mb-3">
<label for="photo_filename" class="form-label">Название фото</label>
<input type="text" class="form-control" id="photo_filename" name="photo_filename" value="{{ guest.photo_filename if guest else '' }}">
</div>
{% if not guest %}
<div class="mb-3">
<label for="base_url" class="form-label">Базовый URL для QR-кода (опционально)</label>
<input type="url" class="form-control" id="base_url" name="base_url" placeholder="https://events.company.com">
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="send_invitation" name="send_invitation" checked>
<label class="form-check-label" for="send_invitation">Отправить приглашение сразу</label>
</div>
{% endif %}
<button type="submit" class="btn btn-primary">Сохранить</button>
<a href="{{ url_for('main.admin_guests') }}" class="btn btn-secondary">Отмена</a>
</form>
</div>
</div>
{% endblock %}
+78
View File
@@ -0,0 +1,78 @@
{% extends "base.html" %}
{% block title %}Гости — InviteMe{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>Гости</h1>
<a href="{{ url_for('main.admin_guest_add') }}" class="btn btn-primary">Добавить гостя</a>
</div>
<form method="GET" class="row g-3 mb-4">
<div class="col-auto">
<input type="text" name="q" class="form-control" placeholder="Поиск по ФИО" value="{{ search }}">
</div>
<div class="col-auto">
<button type="submit" class="btn btn-outline-primary">Найти</button>
</div>
</form>
<table class="table table-striped table-hover">
<thead>
<tr>
<th>ФИО</th>
<th>Отдел</th>
<th>Должность</th>
<th>Email</th>
<th>Фото</th>
<th>QR</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
{% for guest in guests %}
<tr class="{{ 'table-secondary' if not guest.is_active }}">
<td>{{ guest.full_name }}</td>
<td>{{ guest.department }}</td>
<td>{{ guest.position or '-' }}</td>
<td>{{ guest.email }}</td>
<td>
{% if guest.photo_filename %}
<span class="badge bg-success">Есть</span>
{% else %}
<span class="badge bg-warning text-dark">Нет</span>
{% endif %}
</td>
<td>
{% if guest.qr_filename %}
<span class="badge bg-success">Да</span>
{% else %}
<span class="badge bg-secondary">Нет</span>
{% endif %}
</td>
<td>
{% if guest.is_checked_in %}
<span class="badge bg-success">Пришёл</span>
{% else %}
<span class="badge bg-secondary">Не пришёл</span>
{% endif %}
</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>
</form>
<form action="{{ url_for('main.admin_guest_delete', guest_id=guest.id) }}" method="POST" class="d-inline" onsubmit="return confirm('Заблокировать гостя?')">
<button type="submit" class="btn btn-sm btn-outline-danger">Блок</button>
</form>
</td>
</tr>
{% else %}
<tr>
<td colspan="8" class="text-center text-muted">Гости не найдены</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
+49
View File
@@ -0,0 +1,49 @@
{% extends "base.html" %}
{% block title %}Загрузка данных — InviteMe{% endblock %}
{% block content %}
<h1 class="mb-4">Загрузка данных</h1>
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-body">
<h5 class="card-title">1. Загрузить Excel со списком гостей</h5>
<p class="text-muted">Столбцы: Отдел, ФИО, Должность, e-mail, название фото</p>
<form method="POST" enctype="multipart/form-data">
<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>
</div>
<div class="mb-3">
<label for="base_url" class="form-label">Базовый URL для QR-кодов (опционально)</label>
<input type="url" class="form-control" id="base_url" name="base_url" placeholder="https://events.company.com">
</div>
<button type="submit" class="btn btn-primary">Загрузить и сгенерировать QR</button>
</form>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-body">
<h5 class="card-title">2. Загрузить фото гостей</h5>
<p class="text-muted">Фото должны называться точно так же, как указано в столбце «название фото»</p>
<form method="POST" enctype="multipart/form-data">
<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>
</div>
<div class="mb-3">
<label for="zip_file" class="form-label">Или ZIP-архив</label>
<input type="file" class="form-control" id="zip_file" name="zip_file" accept=".zip">
</div>
<button type="submit" class="btn btn-primary">Загрузить фото</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
+65
View File
@@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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') }}">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('main.index') }}">InviteMe</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 session.role == 'admin' %}
<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 %}
{% if session.role in ('admin', 'staff') %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('main.check_in') }}">Вход</a>
</li>
{% endif %}
</ul>
{% if session.role %}
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('main.logout') }}">Выйти</a>
</li>
</ul>
{% endif %}
</div>
</div>
</nav>
<div class="container mt-4">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>
+52
View File
@@ -0,0 +1,52 @@
{% extends "base.html" %}
{% block title %}Отметка входа — InviteMe{% endblock %}
{% block content %}
<div class="checkin-container">
<h1 class="mb-4 text-center">Отметка входа</h1>
<div class="row">
<div class="col-md-6 mb-3">
<div class="card">
<div class="card-body">
<h5 class="card-title">Сканировать QR-код</h5>
<div id="qr-reader" class="qr-reader"></div>
<button id="start-scan" class="btn btn-primary w-100 mt-2">Включить камеру</button>
<button id="stop-scan" class="btn btn-secondary w-100 mt-2 d-none">Выключить камеру</button>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card">
<div class="card-body">
<h5 class="card-title">Или найти по ФИО</h5>
<input type="text" id="search-input" class="form-control" placeholder="Введите фамилию..." autocomplete="off">
<div id="search-results" class="list-group mt-2"></div>
</div>
</div>
</div>
</div>
<div id="guest-card" class="card mt-4 d-none">
<div class="card-body text-center">
<img id="guest-photo" src="" alt="Фото гостя" class="guest-photo mb-3">
<h2 id="guest-name" class="card-title"></h2>
<p id="guest-department" class="lead"></p>
<p id="guest-position" class="text-muted"></p>
<p id="guest-email" class="text-muted"></p>
<div id="check-in-status" class="alert d-none"></div>
<button id="check-in-btn" class="btn btn-success btn-lg w-100">Отметить вход</button>
<button id="reset-btn" class="btn btn-outline-secondary w-100 mt-2">Сбросить</button>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js"></script>
<script src="{{ url_for('static', filename='js/checkin.js') }}"></script>
{% endblock %}
+27
View File
@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Приглашение на {{ event_name }}</title>
</head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #ddd; border-radius: 8px;">
<h2 style="color: #0d6efd;">Приглашение на мероприятие</h2>
<p>Здравствуйте, <strong>{{ guest.full_name }}</strong>!</p>
<p>Вы приглашены на мероприятие <strong>{{ event_name }}</strong>.</p>
<p>Ваш персональный QR-код для входа:</p>
<div style="text-align: center; margin: 20px 0;">
<img src="cid:qr_code_image" alt="QR-код" style="max-width: 250px;">
</div>
{% if additional_text %}
<p>{{ additional_text }}</p>
{% endif %}
<p style="color: #666; font-size: 14px;">
Пожалуйста, предъявите этот QR-код при входе. Не передавайте его другим лицам.
</p>
</div>
</body>
</html>
+26
View File
@@ -0,0 +1,26 @@
{% extends "base.html" %}
{% block title %}Вход — InviteMe{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-4">
<div class="card shadow-sm mt-5">
<div class="card-body">
<h3 class="card-title text-center mb-4">InviteMe</h3>
<form method="POST" action="{{ url_for('main.login') }}">
<div class="mb-3">
<label for="username" class="form-label">Логин</label>
<input type="text" class="form-control" id="username" name="username" required autofocus>
</div>
<div class="mb-3">
<label for="password" class="form-label">Пароль</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary w-100">Войти</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}