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