Задача
До мероприятия команда получает списки из разных источников: от организаторов, артистов, промоутеров, партнёров и внутренних сотрудников. Внутри одного события действуют разные категории доступа и разные финансовые условия. А на входе решение нужно принять за несколько секунд, и при этом одно приглашение не должно сработать дважды.
Обычный «список в таблице» такое не закрывает. Требовалось собрать все списки в один реестр без повторов, сохранить источник каждого приглашения и заранее разослать персональные пропуска. На входе нужны были быстрый проход через QR и ручной поиск на случай разряженного телефона, разделение на Regular, VIP и Backstage, предупреждение о повторном или неизвестном коде и учёт доплаты на месте. А руководителю нужна была картина явки, пока событие идёт.
Решение
Импорт с дедупликацией по трём признакам. Гостей добавляют вручную или загружают из CSV; импорт понимает и запятую, и точку с запятой и сам определяет формат файла. При загрузке телефоны нормализуются, регистр имени приводится к единому виду. Дубликаты ищутся и внутри файла, и относительно уже существующих гостей события — по имени, нормализованному телефону и источнику приглашения. Пропущенные повторы выводятся в результате импорта, а список не засоряется.
Категория — операционный признак. Regular, VIP и Backstage видны в административном списке, в мобильном интерфейсе контроля и в результате сканирования. Статистика считается и по мероприятию целиком, и отдельно по каждой категории: сколько приглашено, сколько прошло, для скольких отмечена доплата.
Два независимых состояния до события и два после. Администратор отдельно видит, подписан ли человек на канал доставки и сформирован ли его QR. Так же отдельно живут статус прохода и статус оплаты: человек может быть в списке, но нуждаться в доплате на входе. Если смешать эти оси, на проходной непонятно, что делать с конкретным гостем.
Рассылка меняет статус только после доставки. Серверная функция генерирует изображение QR, собирает сообщение с названием и временем мероприятия, отправляет приглашение и только после успешной доставки переводит запись в новое состояние. Каналов два: HTML-письмо через Resend и Telegram-бот, который связывает номер гостя с chat ID подписчика; триггеры в базе обновляют состояние подписки у уже существующих записей. Ограничение частоты отправки защищает массовую рассылку от лимитов внешнего API.
Сканер перечитывает состояние перед решением. Камера открывается на весь экран, ZXing получает видеопоток и выбирает доступную заднюю камеру, рамка помогает удерживать код в зоне считывания. Перед изменением статуса приложение заново запрашивает актуальные данные гостя. Это важно, когда на входе работают несколько сотрудников: решение принимается по последнему состоянию записи, даже если гостя только что отметил коллега.
Четыре исхода, различимые без чтения. Проход разрешён; гость уже проходил — со временем первого входа; код неизвестен; техническая ошибка проверки. Результат занимает весь экран и отличается цветом и иконкой, чтобы в темноте и в потоке людей не приходилось вчитываться в мелкий текст. Одна кнопка возвращает к следующему сканированию.
Повтор не создаёт второй проход. При первой успешной отметке прохода сохраняются состояние, точное время входа и связь с гостем и мероприятием. Повторное сканирование новую запись не пишет: сотрудник видит предупреждение и время первоначальной регистрации. Это защищает от передачи приглашения другому человеку и помогает быстро отличить попытку повторного входа от ошибки камеры.
Доплата — действие в одну сторону. Отметить получение денег можно и из полной административной таблицы, и из мобильного интерфейса, но в мобильном сценарии отменить отметку нельзя: случайное повторное нажатие не снимет оплату.
Панель обновляется в реальном времени. Карточка события подписана на изменения гостевых записей через Supabase Realtime: когда сотрудник на входе отмечает человека, список и показатели меняются без перезагрузки страницы. Несколько точек контроля работают с единым состоянием, и риск двойного прохода при почти одновременной проверке одного кода снижается.
Доступы на уровне базы. Клиент собран на React 18, TypeScript, Vite, Tailwind и shadcn/ui; Supabase отвечает за авторизацию, данные, канал обновлений и серверные функции. Секреты рассылок хранятся в Edge Functions и в браузерный код не попадают. В базе работают Row Level Security и функции определения текущей роли: организатор видит и обновляет гостей только в пределах сценария контроля, роль нельзя повысить правкой собственного профиля, а серверные функции отдельно проверяют авторизацию, роль и право работать с выбранным мероприятием. Интерфейс организатора вдобавок скрывает общий список до ввода поискового запроса.
Результат
Клуб получил один реестр вместо переписки со списками. Мероприятие проходит через понятные состояния — подготовка, рассылка, активное событие, завершение, — а история остаётся доступной после закрытия: можно вернуться к прошедшему событию и разобрать фактическую явку.
Сотрудник на входе работает одной рукой при нестабильном освещении: камера, крупный цветной результат, кнопка «дальше». Если телефон гостя разрядился, сотрудник находит человека по фамилии, и проход записывается в ту же историю, что и при QR-проверке.
Руководитель видит приглашённых, прошедших, ожидаемых и явку в процентах: в целом, по каждой категории доступа и по каждому источнику приглашения. Последнее меняет разговор после события: видна фактическая отдача каждого списка — чьи гости дошли, а чьи только заняли места в реестре.
Гостю не досталось ни приложения, ни бумажного билета: персональный код приходит в удобный ему канал и открывается на телефоне.