← Все разделы

TRIDE Sales Screen — витрина и точка продаж на экране устройства

Новый режим экрана для планшетов и TV клуба: в простое — красивая слайд-витрина продуктов, по тапу сотрудника — полноэкранная касса (POS) с продажами, записью на занятия и оплатой, в том числе QR-кодом прямо на экране. Всё строится на переиспользовании существующего стека продаж, касс и Indoor TV.

Версия 1.0 · 20.10.2026 Статус: черновик на согласование Основан на аудите кода frontend/backend от 20.10.2026 Затрагивает: frontend, backend (main), устройства

1.Резюме

Сегодня экран устройства в клубе умеет два вещи: показывать тренировку и крутить Indoor TV. Продажи при этом живут в другом месте — сотрудник должен зайти в приложение, открыть раздел «Продажи → Касса». Предлагаем замкнуть контур прямо на экране у стойки:

  • Витрина (Showcase) — пока устройством не пользуются, на нём автоматически крутится эффектное слайд-шоу продуктов клуба: абонементы, пакеты, товары и услуги по выбранным категориям и фильтрам, с ценами (опционально), клубным брендингом, переходами и анимациями. Витрина работает и как отдельный режим POS-планшета, и как новый тип трека в обычных плейлистах Indoor TV.
  • POS-режим — сотрудник тапает по экрану и быстро идентифицируется: сканирует QR из приложения TRIDE, где он уже авторизован (опциональный fallback — PIN), — и получает большой планшетный дашборд точки продаж: каталог плитками, чек, привязка клиента, скидки и промокоды, продажа членств/пакетов/товаров/услуг, запись на занятие, оплата наличными/терминалом или QR-кодом на экране планшета, который клиент сканирует и оплачивает со своего телефона. Через настраиваемый таймаут бездействия (по умолчанию 5 минут) экран сам возвращается в витрину.

Ключевое продуктовое требование — нулевое дублирование механик. POS-часть — это существующая касса /sales/pos (ClubPos + PosCatalog + SaleReceipt) в киоск-обвязке; оплата — существующие контуры sales.offline.charge и счёт-ссылки /pay/<token> с поллингом статуса; запись на занятие — существующий визард услуги (bookings.admin.create); кассовая смена, фискализация, склад, промокоды — без изменений. Новое — только режим устройства, витрина-слайдер, вход кассира и обвязка таймаута.

Почему это дёшевоТач-механика «экран просыпается по касанию» уже существует (меню в режиме экрана раскрывается по touchstart, layout/menu/Menu.svelte:83), полноэкранная касса с планшетной раскладкой уже написана, QR СБП и платёжные ссылки уже работают, показы контента уже считаются (tv_impressions). Мы собираем готовые блоки в новый сценарий.

2.Как устроено сейчас (ас-ис)

Раздел основан на аудите кода; все пути — реальные файлы репозиториев frontend/ и backend/.

2.1. Режимы экрана устройства и Indoor TV

  • Регистрация и сессия устройства. Планшет/TV-бокс привязывается к клубу подтверждением QR (auth/login/services/auth.requests.js) или автологином по hw_id (club.get.device.token → modules/devices/services/devices.boxes.js). Сессия получает screenmode: true; устройство — документ в devices (screenmode, screenmode_type, indoor_tv, zone_id, club_id, custom_name…).
  • Цикл режима. ClubService.checkMyState() каждые 30 с дергает /devices/getscreenmode → appState.screenMode → App.svelte редиректит на /screen. Диспетчер pages/trainingscreen/ScreenMode.svelte:127–151: есть gamesession → экран тренировки; иначе, если есть активное окно эфира (/devices/getSchedule, опрос 60 с) → Indoor TV (ScheduleMode.svelte); иначе — загрузка.
  • Indoor TV. Расписание окон tv_schedule (дни недели, время, зоны, 17 лэйаутов, слоты: часы/расписание/чат/рейтинг/бегущая строка), плейлисты tv_playlists с треками video | image | stream | url | pulse | challenge | rating. Плеер components/IndoorTV/MainPlayer.svelte синхронизирует все экраны клуба по границе серверной секунды. Контент скачивается локально (DownloadManager), показы считаются в tv_impressions_raw → куб tv_stats_daily. Админка: pages/myClubs/indoortv/ (расписание, программы, музыка, объявления, устройства, аналитика), права indoortv:*.
  • Интерактивность сегодня. Indoor TV полностью пассивен. В режиме экрана скрытое боковое меню раскрывается по наведению мыши или touchstart (тач устанавливает screenModeMenu=60), и сотрудник может уйти в приложение. Автовозврата из приложения в показ с таймаутом нет — значение 300 000 мс нигде не тикает. Киоск-режима и screensaver-контракта нет.
  • Тема. На время экранного режима на контейнер вешается класс .tridescreen — палитра дефолтной тёмной темы Tride, чтобы тема клуба не влияла на тренировочные экраны (css/app.css:217).

2.2. Касса, продажи и платежи сегодня

  • Касса — раздел /sales/pos (pages/myClubs/sales/pos/): слева витрина плитками PosCatalog (фильтры «Все/★/Товары/Абонементы/Услуги», поиск, остатки, варианты товара; данные sales.sellable.list), справа чек PosReceipt → components/sales/SaleReceipt.svelte (1316 строк — ядро продажи: клиент, позиции, скидка на чек, промокод, сертификат, сплит-оплата, отложенные чеки). На узких экранах — вкладки «Каталог/Чек», т.е. планшетная раскладка фактически готова.
  • Сканеры: HID-сканер со всей страницы + камера (createScanner), поиск товара по коду goods.find_by_code, выдача самовывоза по QR tride://pickup/<код> (sales.sold_items.issue).
  • Оплата офлайн: sales.offline.charge — наличные/терминал/перевод, разбивка (split), «получено» → сдача, выбор кассы; при обязательной смене (cash_shift_required) продажа блокируется до открытия смены (cash.shift.open). Идемпотентность через idempotency_key.
  • Оплата ссылкой/QR: sales.offer.create + sales.offer.issue → счёт /pay/<token>; экран SaleLinkResult уже умеет показывать QR, отправлять ссылку push/SMS/чатом и поллить статус каждые 5 с. У клиента — виджеты CloudPayments / ЮKassa (включая QR СБП) / Яндекс Пэй.
  • Запись через продажу: услуга в чеке требует выбора занятия (SaleSessionPicker, schedule.list.public), перед оплатой создаётся бронь bookings.admin.create с payment:'offer'.
  • Кассы/смены/фискализация: booking_cash_registers/operations/shifts, ККТ — АТОЛ (локальный web-server), OrangeData, ОФД; чеки booking_fiscal_receipts.

2.3. Продукты: что продаём

Единый каталог для витрины и POS — уже существующие сущности (SELLABLE_TYPES: membership | product | service | gift_certificate | booking):

СущностьКоллекцияЧто это / ключевые настройки
Членства и пакетыbooking_membership_productsВиды: unlimited (безлимит на срок), limited_visits (N визитов), class_pack (пакет занятий), limited_time (минутный пул). Настройки: цена/период/валюта, визиты и лимиты, заморозка, активация, автопродление (auto_renewal), включённые услуги/категории, сетевой доступ (network_policy: скоуп, со-доступ, гостевые визиты), видимость (is_public, is_featured, status), purchase_limits, ограничения по времени/полу/возрасту.
Товарыbooking_productsТипы retail/rental/merch/consumable/equipment; варианты, штрихкоды/GTIN, маркировка, цены (базовая/скидочная/членская), остатки (booking_stock_levels), витринные флаги.
Услугиbooking_servicesПерсональные/групповые/spa/аренда; длительность, цена, политика списания визита (visit_charge).
Категорииbooking_product_categoriesДерево; применимость к членствам/товарам/услугам; флаги показа, правило витрины.
Сертификаты, промокоды, ссылкиbooking_gift_certificates, booking_promocodes, booking_product_linksСкидки/подарки; публичные ссылки покупки /buy/<token> с ценой-промо и режимом подарка.

Продажа в любом канале (приложение клиента, ссылка, касса) сходится в booking_sale_offers → booking_orders → booking_transactions → фулфилмент (booking_sold_items, активация booking_memberships); журнал продаж sales.ledger.* уже показывает канал каждой продажи, у позиций есть источник source: staff|shop|link|desk|assistant|pos.

2.4. Что мешает сегодня (гэпы)

  • POS живёт «в приложении сотрудника»: нужен вход, меню, раздел — длинный путь от «клиент стоял у стойки» до «оплачено».
  • Экраны клуба пассивны: продукты нигде автоматически не показываются; витрины товаров в приложении (/showcase) нет на физическом носителе в клубе.
  • Механика «тапнул → поработал → ушло в показ» отсутствует: нет таймаута возврата, нет киоск-контракта.
  • Идентификация продавца на общем устройстве невозможна: сессия устройства не даёт прав/атрибуции конкретного кассира.
  • QR-оплату нельзя показать клиенту «здесь и сейчас на большом экране» — SaleLinkResult заточен под рабочий стол сотрудника.

3.Цели, метрики, не-цели

≤ 60 сек
от «клиент подошёл к стойке» до «оплачено наличными/терминалом» на экране продаж
≤ 2 мин
цикл оплаты по QR: сотрудник показал → клиент сканировал → оплатил → экран успеха
≥ 25%
доля офлайн-продаж клуба, проходящих через Экран продаж, через 2 месяца после включения

Не-цели (осознанно не делаем в этом плане): полноценный киоск самообслуживания с покупкой без сотрудника (кандидат в Ф3), новая фискализация или платёжный провайдер, автоматическая печать на Bluetooth-принтерах, изменения в модель членств/скидок.

4.Обзор решения: два состояния одного экрана

Устройство (планшет на стойке, TV, Tridebox) переводится в новый режим — «Экран продаж» (devices.pos_mode, включается в админке). Для него маршрут /screen вместо тренировочного контура рендерит новый диспетчер PosMode.svelte с конечным автоматом из четырёх состояний:

1ВитринаСлайд-шоу продуктов, часы, бегущая строка, брендинг клуба. Пассивна, бесконечна, работает без интернета по кэшу.
→ тап
2Вход кассираQR на экране → сотрудник сканирует из приложения TRIDE (или резервный PIN). Личность и права — из его сессии; связывается с кассовой сменой.
→
3POS-дашбордКаталог плитками + чек + клиент + оплата (наличные/терминал/QR на экране) + запись на занятие.
→ таймаут 5 мин
4ВозвратПредупреждение → авто-отложение незакрытого чека → выход кассира → витрина. Данные клиента исчезают с экрана.

Переходы состояний локальные (без перезагрузки), с плавной анимацией. Из витрины по тапу не попадают в приложение напрямую: экран входа кассира — единственная дверь, что закрывает и приватность, и атрибуцию продаж.

СовместимостьРежим «Экран продаж» не отменяет существующие контуры: тренировочные зоны продолжают работать как раньше (screenmode_type: workouts…), Indoor TV — как раньше. pos_mode — исключающий флаг устройства: если он включён, тренировочный контур на этом устройстве не используется (в админке показываем предупреждение).

5.Часть А. Витрина (Showcase)

5.1. Данные и правила отбора продуктов

Слайды собирает сервер: новый экшен devices.getPosShowcase (сессия устройства) раз в 5 минут отдаёт подготовленный набор слайдов — фронт только рендерит и кэширует. Серверная сборка даёт: единые правила цен, уважение сетевых скоупов и статусов, отсутствие лишней логики на устройстве.

  • Отбор: продукты клуба по конфигу витрины — категории (мультиселект из booking_product_categories), типы (membership | product | service), только избранные (is_featured), ручной список product_ids, фильтры «только с фото», «только в наличии».
  • Безопасность витрины: только status: active и is_public: true в рамках club_scope устройства; правила видимости те же, что у клиентского магазина.
  • Цены: настройка показа — не показывать / базовая / со скидкой (pricing.sale_price); опция «членская цена» вторым числом. Все суммы — Decimal-строки, форматирование на сервере.
  • QR покупки (Ф2): если у продукта есть публичная ссылка booking_product_links (или создаём автоматически по флагу), слайд получает QR на /buy/<token> — клиент может купить со смартфона без сотрудника.
  • Карточка слайда: фото (обложка продукта), название, короткое описание (генерируется из ключевых характеристик: срок, визиты, заморозка…), цена, бейджи (★ Избранное, «Скидка», «Новинка»).

5.2. Макеты слайдов

Четыре макета, ротация настраивается; все — на семантических токенах дизайн-системы и клубной теме (для витрины класс .tridescreen не вешаем — это клиентская поверхность клуба). Ниже — дизайн-макеты в пропорции экрана 16:9.

T
IRONSTAR
14:32
пятница, 20 октября
ЧЛЕНСТВО · 12 МЕСЯЦЕВ
Безлимит PREMIUM
Все зоны клуба, групповые занятия, пул тренажёрного зала и доступ в сеть клубов
⏱ 12 месяцев ❄ Заморозка 30 дней +1 гостевой визит 🔒 Все зоны
Цена клуба
5 490 ₽ 4 990 ₽
−9% до 31 октября
Купить на ресепшене
или отсканируйте и
купите со смартфона
🔥 День открытых дверей 26 октября · 🏊 Бассейн: утренние группы свободны · 🎁 Приведи друга — месяц в подарок
Макет «Герой» (hero) — один продукт крупно: фото-фон с медленным Ken Burns-зумом, ключевые преимущества чипами, цена со скидкой, бегущая строка объявлений (переиспользуем tv_anouncements), индикатор прогресса слайда. Тап в любом месте — вход для кассира (подсказка «Кассир: коснитесь экрана» выводится мягко мигающим лейблом первые 10 с после смены слайда).
Витрина товаров Питание и аксессуары
6 из 24 позиций · обновлено 14:29
★
🥤
Протеин Whey 900 г
3 490 ₽
На складе: 12
🧤
Перчатки для зала
1 290 ₽
На складе: 5
★
🧻
Полотенце клубное
690 ₽
На складе: 30
🚰
Шейкер 700 мл
590 ₽
На складе: 8
⚡
Изотоник 600 г
890 ₽ 1 090 ₽
На складе: 4
🎒
Сумка для тренировок
2 290 ₽
На складе: 3
← проводите по экрану, чтобы листать
Макет «Мозаика» (mosaic) — категория товаров сеткой 2×3: фото-иконки, цены, остатки (если включены). Плитка «★» — избранные. Свайп листает вручную (сброс авто-ротации на 30 с).
Абонементы клуба прайс · октябрь 2026
БЕЗЛИМИТ
12 месяцев49 900 ₽
6 месяцев29 900 ₽
1 месяц5 990 ₽
ПАКЕТЫ
10 визитов7 900 ₽
5 персональных11 500 ₽
Минутный пул 600 мин6 400 ₽
Полный прайс и рассрочка — на ресепшене и в приложении
Макет «Прайс-лист» (pricelist) — спокойная таблица по категориям (безлимиты слева, пакеты справа), в стилистике UpcomingSchedule. QR ведёт на полный каталог покупки. Подходит для «дежурного» слайда между hero-слайдами.

Промо-слайд (Ф2)

Ручной слайд-конструктор в админке: заголовок, текст, фон (фото/градиент), бейдж акции, дата окончания («осталось N дней»), QR на покупку или лид-форму. Промо-слайды вставляются в ротацию с заданной частотой (например, каждый 3-й слайд).

5.3. Переходы, эффекты, ритм

  • Длительность слайда: 8–30 с (по умолчанию 12 с), отдельно для hero/прайса; прогресс-полоса как в OverviewReel (CSS animation … linear forwards + --dur).
  • Переходы: fade (600 мс кроссфейд — дефолт), slide (горизонтальный сдвиг с cubic-bezier(0.25,0.9,0.3,1)), zoom (масштаб 1.04→1 с fade). Выбор глобальный + per-layout. Все уважают prefers-reduced-motion.
  • Эффекты внутри слайда: Ken Burns для фото-фонов; каскадное появление чипов и цены (stagger 80 мс); мягкая пульсация CTA.
  • Звук: нет (клиентская зона). Музыка Indoor TV на POS-устройстве не играет.
  • Синхронизация: локальный таймлайн (в отличие от эфирного MainPlayer синхронизация по серверной секунде не нужна — у каждого устройства свой зритель).

5.4. Настройки витрины (админка)

Настраивается на устройстве (переопределение) с дефолтом клуба. Место в админке: Продажи → Экран продаж (новая вкладка в разделе продаж) + быстрый тумблер в карточке устройства Indoor TV. Право — новое sales:pos_screen:config.

Экран продаж · Планшет ресепшенаonline · v7.6.1
Устройство: TAB-01 · зона Ресепшн
ВИТРИНА · ЧТО ПОКАЗЫВАТЬ
Безлимиты ×Пакеты ×Питание ×+ категория
ПОКАЗ
Длительность слайда12 сек
Переход: fadeslidezoom
Макеты: героймозаикапрайс
POS · КАССИРЫ И ТАЙМАУТ
Возврат в витрину после5 мин бездействия
ЖИВОЕ ПРЕВЬЮ
ЧЛЕНСТВО
Безлимит PREMIUM
12 мес · заморозка · гостевой
4 990 ₽
Сохранить и применить
Админка настройки — TrideForm-совместимая форма (декларативные поля, включая мультиселект категорий и слайдер длительности) + живое превью витрины на реальном компоненте (как IndoorTVPreviewModal рендерит настоящий ScheduleMode). Кнопка «Проверить на устройстве» отправляет обновление и показывает статус доставки.

5.5. Витрина внутри обычного Indoor TV

Для больших телевизоров, уже крутящих эфир: новый тип трека products в tv_playlists. Трек несёт конфиг витрины (категории/макет/длительность) и рендерится в main-слоте лэйаута тем же ShowcasePlayer. Так «продажи» попадают в общую ротацию клуба (видео → продукты → рейтинг → …) без нового расписания. Показы трека считаются в существующей аналитике Indoor TV с детализацией по продуктам.

Аналитика витриныКаждый показ слайда (≥50% длительности, как у треков) уходит в существующую очередь impressionsQueue с kind:'showcase_slide' и product_id. Агрегация statsmanager пополняется разрезом «продукт × устройство × день». В Ф2 — вкладка в аналитике Indoor TV: показы, уникальные слайд-сессии, сканирования QR (переходы по buy-ссылкам уже считаются в booking_product_links.stats).

6.Часть Б. POS-режим по тапу

6.1. Вход и идентификация кассира

Тап в любом месте витрины → экран входа. Это принципиальный гейт: витрина публична, тапают все — включая клиентов и детей.

  • Основной механизм — QR из приложения сотрудника (без новых креденшелов). В TRIDE нет паролей (вход только по СМС-коду), поэтому «обычный логин» на общем планшете непригоден. Зато в системе уже есть паттерн подтверждения по QR — так регистрируются экраны (confirmScreenAuthRequest, auth/login/services/auth.requests.js): экран показывает QR, авторизованный сотрудник сканирует, бэкенд связывает сессию с сотрудником. Инвертируем его для POS: планшет показывает живой QR «Отметиться на смене» → сотрудник сканирует из приложения TRIDE (где уже залогинен) → pos.session.confirm создаёт POS-сессию с его uid → планшет входит в дашборд. Личность и права приходят из настоящей сессии сотрудника, ничего нового выдавать и хранить не нужно.
  • Резервный PIN (опция на устройство, по умолчанию выключен). Для клубов, где телефон на ресепшне недоступен: плитки сотрудников + PIN 4–6 цифр (users.pos_pin, хэш, задаётся в карточке сотрудника; 3 ошибки → пауза 60 с + аудит).
  • Кассовая смена: сразу после входа — если cash_shift_required и смена не открыта, полноэкранная карточка «Открыть смену» (cash.shift.open); если смена открыта другим кассиром — предупреждение с выбором: подхватить (Ф2: смена на несколько кассиров) или открыть на своей кассе.
  • Атрибуция и токены — двухуровневая модель: токен устройства остаётся непривилегированным (витрина, getPosShowcase, создание запроса входа) и продавать не может в принципе; POS-токен кассира (сессия с uid, источник pos_screen, TTL = таймаут бездействия, продлевается активностью) появляется только после подтверждения QR/PIN — все вызовы sales.* идут с ним. Продавец, права и журнал продаж получаются «бесплатно», транспорт не меняется.
Отметиться на смене
Наведите камеру приложения TRIDE · экран вернётся в витрину автоматически
 ожидаем сканирование… 29 с
QR одноразовый и живёт 60 с,
обновляется автоматически
КАК ЭТО РАБОТАЕТ
1 Откройте приложение TRIDE на своём телефоне
2 Сканер QR в меню приложения
3 Подтвердите «Отметиться на смене»
Сотрудник видится со своими правами; к смене привязывается его касса. Один кассир на устройство, смена кассира — повторным сканированием.
Цифровой PIN — резервный вход
Вернуться в витрину
Экран входа кассира. Основной путь — QR из авторизованного приложения сотрудника (инверсия существующего паттерна подтверждения экранов confirmScreenAuthRequest): никаких новых паролей и выдач, личность и права берутся из сессии сотрудника. Резервный PIN-вход — опция на устройство (по умолчанию выключена). 30 с бездействия на этом экране — авто-возврат в витрину.

6.2. POS-дашборд

Ядро — существующие PosCatalog + SaleReceipt (режим inline, источник продажи pos); новый компонент-обвязка добавляет киоск-хедер, крупные тач-таргеты и быстрые действия. Раскладка — как у кассы: каталог слева (3fr), чек справа (2fr); на вертикальном планшете — вкладки.

T
IRONSTAR · Планшет ресепшена
Смена №142 открыта 09:02 · касса Основная
● смена открыта М Мария 14:32 ⤺ Записать ⌕ Клиент ⛶ В витрину
Все★ТоварыАбонементыУслуги ⌕ Поиск или сканер штрихкода…
★
♾
Безлимит PREMIUM 12 мес
49 900 ₽
Членство
🎟
10 визитов
7 900 ₽
Пакет
🧑‍🏫
Персональная тренировка
2 300 ₽
Услуга · запись
🥤
Протеин Whey 900 г
3 490 ₽
Остаток: 12
🚿
Разовое посещение
990 ₽
Услуга
🎁
Сертификат 5 000 ₽
5 000 ₽
Подарок
🧻
Полотенце клубное
690 ₽
Остаток: 30
⚡
Изотоник 600 г
890 ₽
Остаток: 4
Чек SALE-261020-0007 новый
И
Игорь Сомов
+7 9•• ••• 45-10 · 3 членства
✕
Безлимит PREMIUM 12 мес49 900 ₽
Протеин Whey × 13 490 ₽
Промокод AUTUMN −10%−5 339 ₽
Итого48 051 ₽
Наличные Терминал ▣ QR-код
Отложить чекСкидка чекаДобавить клиента
POS-дашборд (планшет landscape). Киоск-хедер: клуб, смена, кассир, часы, «Записать» и «Клиент» (быстрые действия), «В витрину». Каталог — плитки с фильтрами и поиском/сканером. Чек — клиент, позиции, промокод, итог и три кнопки оплаты. Компоненты и логика — из /sales/pos, изменён только обвёрткой и размером тач-таргетов.

6.3. Продажа (переносится 1:1 из кассы)

  • Клиент: поиск по имени/части телефона (clients.search), создание на лету, «без клиента» для товаров (если разрешено клубом), корпоративные/семейные счета и со-оплата — всё как в SaleClientPicker.
  • Позиции: товары (варианты, дробное количество, маркировка «Честный знак»), членства/пакеты, услуги (выбор занятия), сертификаты; ручная цена/скидка строки при праве; сетевой доступ (network_access) для членств.
  • Скидки/промо: скидка на чек с причиной, промокод, сертификат — пересчёт всегда серверный (sales.preview).
  • Сканер: HID и камера планшета; QR самовывоза клиента tride://pickup/… — выдача прямо с экрана.
  • Отложенные чеки: отложить/вернуть (sales.offer.hold/resume) — критично для стойки.

6.4. Оплата

Наличные / терминал — sales.offline.charge: сплит, «получено» → сдача, выбор кассы, блокировка без открытой смены. Фискализация — по контуру клуба (club_kkt/облако); локальный АТОЛ web-server на планшете недоступен — отмечаем как ограничение, ручной чек через sales.receipt.mark_manual/send.

QR на экране планшета — главный новый сценарий:

Чек готов→ «QR-код»→ createSaleOffer + issue→ Полноэкранный QR /pay/<token>→ клиент сканирует камерой→ оплата на смартфоне (СБП/карта/виджет)→ поллинг 5 с→ экран успеха
  • QR-экран — киоск-вариант SaleLinkResult: крупный QR, сумма, таймер жизни ссылки, статус «ожидаем оплату…», кнопки «Отправить ссылку» (push/SMS/чат — sales.offer.send) и «Отмена».
  • Клиент по ссылке попадает на привычный /pay/:token: виджет ЮKassa (включая QR СБП), CloudPayments, Яндекс Пэй — без новых платёжных интеграций.
  • Таймаут бездействия во время активного поллинга не срабатывает; счёт unpaid по истечении TTL гаснет, чек возвращается в дашборд.
Оплата по QR · счёт SALE-261020-0007
48 051 ₽
Наведите камеру телефона — откроется оплата (СБП, карта, Яндекс Пэй)
⏱ осталось 23:41  ожидаем оплату…
✉ Отправить ссылку ← Назад к чеку
Оплачено 48 051 ₽
Терминал · чек 142
Сдача: 1 949 ₽ · Продавец: Мария
Безлимит PREMIUM активирован Игорю · Протеин — к выдаче на складе (QR выдачи отправлен клиенту)
✉ Отправить чек клиенту
возврат в кассу через 12 с
Слева — оплата по QR: клиент сканирует большой QR со своего телефона, оплата проходит в знакомом контуре /pay/<token>. Справа — экран успеха: сумма, способ, сдача, что активировано/к выдаче, отправка чека, авто-возврат в кассу с отсчётом.

6.5. Запись на занятие

Быстрое действие «Записать» в хедере: выбор клиента (тот же поиск) → выбор вида услуги → ближайшие занятия из schedule.list.public (карточки «сегодня/завтра», свободные места) → подтверждение. Бесплатная запись — bookings.admin.create напрямую; запись с оплатой — позиция-услуга в чеке с последующим тем же сценарием оплаты. Используем существующий флоу SaleSessionPicker в киоск-обвязке.

6.6. Таймаут бездействия и возврат в витрину

  • Таймер: 5 минут по умолчанию (настройка 1–30 мин); активность = тач/скролл/клавиатура/сканер/открытая модалка с полем. Видимый обратный отсчёт появляется за 60 с («Продлить сеанс?» — большие кнопки «+5 мин» / «Завершить»).
  • Незакрытый чек: при таймауте с позициями — авто-отложение (sales.offer.hold) с тостом «Чек отложен — заберите в журнале продаж»; пустой чек — сброс.
  • Активный платёж: поллинг unpaid-счёта продлевает таймаут; после успеха/отмены — обычный отсчёт.
  • Выход: POS-токен отзывается (pos.session.end), экран гаснет в витрину (fade 400 мс). Данные клиента и чека исчезают с экрана — требование приватности.
  • Жёсткие случаи: разрыв сети — баннер «Нет сети: продажа невозможна», черновик чека хранится в памяти до восстановления; перезагрузка устройства — витрина стартует автоматически (ScreenRecoveryService уже это умеет).

7.Техническая архитектура

7.1. Фронтенд

Файл (новый/изменённый)СтатусСодержание
pages/trainingscreen/PosMode.svelteновыйДиспетчер автомата: витрина ⇄ вход кассира ⇄ дашборд; опрос getPosShowcase (5 мин) и getscreenmode (уже есть); nosleep, сокеты; не вешает .tridescreen (клубная тема).
pages/trainingscreen/ScreenMode.svelteизменитьВетка screenModePos перед тренировочной/эфирной (стр. ~143).
components/Posscreen/ShowcasePlayer.svelteновыйТаймлайн слайдов, переходы (fade/slide/zoom), прогресс-бары, свайп, Ken Burns, impressions. По образцу Screen/overview/OverviewReel.svelte.
components/Posscreen/slides/*новыйHeroSlide / MosaicSlide / PricelistSlide / PromoSlide — чистая презентация данных getPosShowcase.
components/Posscreen/PosLogin.svelteпереиспользованиеQR «Отметиться на смене» (одноразовый, 60 с, автообновление) + статус подтверждения; резервный PIN-вход; открытие смены.
components/Posscreen/PosDashboard.svelteпереиспользованиеКиоск-обвязка над PosCatalog + SaleReceipt (inline, source pos); хедер с быстрыми действиями; увеличенные тач-таргеты.
components/Posscreen/PosQrPayment.svelteпереиспользованиеКиоск-вариант SaleLinkResult: большой QR, поллинг, отправка ссылки.
components/Posscreen/PosSuccess.svelteпереиспользованиеКиоск-вариант экрана успеха кассы + авто-возврат.
store/appState.js, lib/tride.app.jsизменитьscreenModePos из devices.getscreenmode (+pos_mode); POS-токен кассира в памяти (инжект в execPost для sales-вызовов).
pages/myClubs/sales/pos/PosScreenSettings.svelteновыйАдминка настроек (TrideForm) + живое превью; вкладка «Экран продаж» в разделе Продажи.
components/IndoorTV/MainPlayer.svelteизменить (Ф2)Тип трека products → рендер ShowcasePlayer в main-слоте эфира.

Дизайн: только семантические токены (bg-surface-*, text-text-*, rounded-card, shadow-elevation-*), Montserrat, TrideButton/Chip для POS-части; витрина дополнительно использует клубную тему и крупные слайдовые шрифты. Локали — гранулярные файлы locales/ru/pages/my_clubs/sales/posscreen.json и т.д., все строки через $format.

7.2. Бэкенд и модель данных

// devices (существующий документ устройства) — добавить:
{
  pos_mode: true,                 // режим «Экран продаж» (эксклюзивен с тренировочным контуром)
  pos_config: {                   // конфиг витрины и POS (переопределяет клубный дефолт)
    showcase: {
      categories: [ObjectId],     // booking_product_categories
      types: ['membership','product','service'],
      featured_only: false, in_stock_only: true, with_photo_only: true,
      product_ids: [ObjectId],    // ручной выбор (опционально)
      slide_duration_sec: 12, transition: 'fade',           // fade|slide|zoom
      layouts: ['hero','mosaic','pricelist'],               // ротация макетов
      show_prices: 'sale',        // none|base|sale
      show_member_price: false, show_qr_buy: false,         // QR /buy/ на слайдах (Ф2)
      show_clock: true, show_ticker: true
    },
    pos: {
      inactivity_timeout_min: 5, pin_fallback: false,  // QR-вход из приложения — основной; PIN — опция
      cash_register_id: null,     // фикс. касса устройства (иначе выбор/дефолт)
      success_autoclose_sec: 15
    }
  },
  pos_session: { uid, started_ts, expires_ts }   // активный кассир (для аудита и одного входа)
}

// users — pos_pin (хэш pbkdf2/bcrypt) ТОЛЬКО если на устройстве включён pin_fallback
// clubs.settings.pos_screen — клубный дефолт pos_config (тот же формат)
// tv_playlists.tracks[] — новый type: 'products' с вложенным showcase-конфигом (Ф2)
// tv_impressions_raw — kind: 'showcase_slide', payload: { product_id, layout, device_id }
  • Сборка слайдов — сервис в modules/sales/services/ (pos_showcase.js): фильтрация каталога по конфигу, публичность/статус/скоуп, цены (Decimal), генерация карточек слайдов. Кэш 5 мин на устройство.
  • POS-сессия кассира — modules/devices/services/devices.pos.js: pos.session.request (устройство создаёт одноразовый QR-запрос, TTL 60 с) → pos.session.confirm (вызов из приложения сотрудника с его токеном; проверка прав sales:offers:create, открытие/проверка смены) → выдача POS-токена (сессия в token_sessions, uid кассира, источник pos_screen, TTL) — инверсия существующего паттерна confirmScreenAuthRequest. Продление активностью, конец по таймауту/выходу; резервная ветка PIN — только при pin_fallback.
  • Атрибуция продаж — без миграций: sold_items.source: 'pos' уже существует; в sale_offer.origin добавляем тип pos_screen c device_id (расширение enum, backward-compatible).
  • Аудит — коллекция pos_sessions_log (запрос/подтверждение/start/end, uid/device/ts/причины), ошибки резервного PIN — rate limit + запись.

7.3. API (все — POST, как принято)

ЭкшенСтатусОписание
devices.getscreenmodeдополнить+ pos_mode, + версия конфига (для мгновенного обновления витрины).
devices.getPosShowcaseновыйСлайды витрины для устройства (needauth по сессии устройства).
devices.pos.session.request / confirm / extend / endновыйДвухшаговый вход кассира: устройство создаёт одноразовый QR-запрос; confirm вызывается из приложения сотрудника (его токен) и выдаёт POS-токен; продление активностью; выход/таймаут. Паттерн — инверсия confirmScreenAuthRequest.
club.pos.config.get / updateновыйКлубный дефолт и конфигы устройств (право sales:pos_screen:config).
sales.sellable.list / preview / offer.* / offline.charge / offer.hold|resume|send, goods.find_by_code, sales.sold_items.issue, clients.search, schedule.list.public, bookings.admin.create, cash.shift.*, cash.registers.list, sales.receipt.mark_manual|sendкак естьВся продажа, запись, касса, чеки — без изменений.
club.indoortv.playlists.*дополнить (Ф2)Трек type:'products'.

7.4. Права (RBAC)

  • sales:pos_screen — вход кассиром на экране продаж (добавить в пресеты reception, cashier, sales_manager).
  • sales:pos_screen:config — настройка витрин/устройств (в пресет club_manager).
  • Существующие продажи/касса/скидки — используются как есть; витрина устройств — под indoortv:devices для тумблера pos_mode в списке экранов.

7.5. Сети клубов

Устройство принадлежит одному клубу; витрина показывает продукты, действующие в этом клубе (включая сетевые продукты с club_scope, куда клуб входит). Клиент ищется по всей сети клиента (как в кассе). Продажа сетевого членства с со-доступом — тем же UI, что и в кассе. Групповая витрина (один контент на все филиалы сети) — Ф2 через клубный дефолт на группе.

8.Безопасность и приватность

  • Публичная витрина — только публичные продукты активного статуса; цены по настройке; никакой персональной информации на слайдах.
  • Гейт кассира — QR-подтверждение из авторизованного приложения сотрудника (одноразовый запрос 60 с); POS-токен короткоживущий, живёт только в памяти вкладки, отзывается на таймауте/выходе. Резервный PIN — опция (хэш, 3 попытки → cooldown 60 с → аудит).
  • Приватность на экране — авто-возврат очищает чек и клиента с экрана; на экране успеха телефон маскируется; таймаут — гарантия, что карточка клиента не висит на планшете у стойки бесконечно.
  • Деньги — все существующие гарантии сохраняются: идемпотентность offline-проведения, блокировка без смены, журнал кассовых операций, фискализация; QR-ссылки — одноразовые 32-байтные токены офферов с TTL.
  • Устройство — POS-режим не расширяет прав самой device-сессии: она ходит только в витринные экшены (getPosShowcase, pos.session.request, impressions) и продавать не может в принципе — продажа возможна только при активной POS-сессии кассира; экран не даёт выйти в общий интерфейс приложения (меню в режиме экрана уже скрыто, добавляем жёсткий запрет навигации для pos_mode). Кто зарегистрировал устройство в режим — не влияет на продажи, фиксируется только в аудите настроек.

9.Граничные случаи

СлучайПоведение
Нет сети у планшетаВитрина — по кэшу слайдов (последний успешный getPosShowcase + HTTP-кэш изображений). POS — баннер «Нет сети», черновик чека в памяти, продажа недоступна (транзакция требует БД).
Клиент сам тапнул витринуЭкран входа кассира; без подтверждения QR из приложения сотрудника дальше не пройти; 30 с бездействия → обратно в витрину.
Оплата пришла после таймаутаПоллинг активного счёта не даёт таймауту сработать; если счёт всё же оплачен после возврата — на витрине всплывает тост «Счёт … оплачен» и пуш кассиру, продажа видна в журнале.
Два планшета, одна сменаРазрешено: операции обеих устройств ложатся в одну смену, у каждой свой кассир/POS-сессия; device_id в origin различает устройства.
Смена истекла/закрыта при активном POSКак в кассе: следующая продажа требует открыть новую смену (политика requireOpenShiftForSale).
Товар закончился во время показаСлайд убирается при следующем обновлении (5 мин); в POS остаток 0 блокирует добавление (как сейчас).
Вертикальный планшетВитрина — макеты в портретной сетке; POS — вкладки Каталог/Чек (раскладка уже поддерживается).
Обновление приложения на устройствеОкна autoupdate боксов не затрагиваются; после перезагрузки ScreenRecoveryService возвращает в витрину.
У сотрудника нет телефона / разряженРезервный PIN-вход (если включён на устройстве); иначе — вход с любого устройства ответственного (начальник смены) или обычная касса на десктопе.

10.План реализации

ФазаСоставОценкаКритерий готовности
Ф0. Подготовка Утверждение плана и макетов; спайк POS-сессии кассира (транспорт токена); финальная схема pos_config; выбор 2–3 пилотных клубов. 3–5 д Прототип входа кассира на стенде; подписанные макеты.
Ф1. MVP Экрана продаж devices.pos_mode/pos_config + getPosShowcase + pos.session.request/confirm (QR-вход из приложения); PosMode с витриной (hero/mosaic/pricelist, fade/slide), вход кассира по QR, дашборд на базе кассы, offline-оплата и QR-оплата на экране, таймаут + авто-hold, админка настроек (клуб+устройство), impressions витрины, локали ru/en. Резервный PIN — опция, по умолчанию выключена. бэк 8–10 д · фронт 12–15 д · QA 5 д Пилот: планшет на ресепшн клуба продаёт все типы продуктов, QR-цикл ≤ 2 мин, возврат в витрину работает, продажи в журнале с каналом «Экран продаж».
Ф2. Витрина в эфире и удобства Трек products в Indoor TV; промо-слайды (редактор); макет-«карусель» и zoom-переходы; QR покупки на слайдах (/buy); переключение кассира без выхода; групповые дефолты для сетей; вкладка аналитики витрины; локаль fr. 10–12 д суммарно Продукты крутятся на большом TV клуба; конверсия показ→QR-скан измерима.
Ф3. Кандидаты (по результатам) Киоск самообслуживания (каталог+покупка без сотрудника); Bluetooth-принтеры чеков; звуки/голосовые подсказки; deep-аналитика «показ → продажа». отдельный план —
Риски① Права device-сессии: без POS-сессии кассира экран не должен уметь продавать — тестируем отдельно. ② Фискализация с планшета: локальный АТОЛ недоступен — пилотим клубы с club_kkt/ручными чеками. ③ Разнообразие Android-планшетов: wake-по-тапу и nosleep проверяем на целевых моделях. ④ Приватность: авто-возврат — обязательное требование приёмки, а не «желание».

11.Открытые вопросы (для согласования)

  1. Идентификация кассира: предложено — QR из приложения сотрудника как основной (без новых креденшелов), PIN как выключенная по умолчанию опция. Нужен ли вообще PIN-фолбэк в Ф1, или отложить до запроса клубов? Альтернатива на будущее — карта/браслет сотрудника.
  2. Показ цен на витрине — по умолчанию включать или скрывать (часть клубов не афиширует прайс)? Предложение: дефолт «скрыть», включается на устройстве.
  3. QR покупки на слайдах (клиент покупает сам): включаем в Ф1 или Ф2? Предложение: Ф2 — сначала привыкаем к показу.
  4. Таймаут по умолчанию — 5 минут подходит? Нужен ли ночной режим (планшет полностью гасит экран ночью)?
  5. Одна смена — много кассиров: нужна ли в Ф1 «общая смена стойки» или достаточно схемы «каждая продажа за своим кассиром»?
  6. Печать чеков: достаточно ли существующей фискализации клуба, или Bluetooth-принтер — реальное требование ресепшена (тогда это Ф3 с железом)?
  7. Место настроек в админке: «Продажи → Экран продаж» (предложение) или вкладка в Indoor TV?
  8. Витрина и эфир одновременно: нужен ли сценарию «табло у входа» отдельный лэйаут-слот витрины (маленькая колонка цен сбоку от видео)?
  9. Пилотные клубы: какие 2–3 клуба берём и на каком оборудовании (модели планшетов)?

12.Приложение

12.1. Карта переиспользования

ВозможностьПереиспользуемНовое
Каталог плитками, фильтры, поискPosCatalog.svelte, sales.sellable.listкиоск-размеры
Чек: клиент, скидки, промо, позицииSaleReceipt.svelte (inline, source pos), sales.preview—
Оплата наличные/терминалsales.offline.charge, кассы/смены—
Оплата QRsales.offer.create/issue/send, /pay/:token, поллинг SaleLinkResultполноэкранный QR-экран
Запись на занятиеSaleSessionPicker, bookings.admin.createкнопка «Записать»
СканерыcreateScanner, goods.find_by_code, tride://pickup—
Витрина-слайдерпаттерны OverviewReel, UpcomingSchedule, Clock, AnnouncementPlayerShowcasePlayer + слайды
Аналитика показовimpressionsQueue → tv_impressions → statsmanagerразрез product×device
Режим устройстварегистрация/автологин устройства, getscreenmode, ScreenRecoveryServicepos_mode, PosMode.svelte
Киоск-механика «тап → работа → авто-возврат»зацепка touchstart меню экранаавтомат состояний + таймаут + POS-сессия

12.2. Словарь

  • Экран продаж (Sales Screen) — устройство в режиме pos_mode: витрина + POS.
  • Витрина (Showcase) — слайд-шоу продуктов в простое.
  • POS-сессия — период работы кассира на устройстве между входом и таймаутом/выходом.
  • Счёт-ссылка — booking_sale_offers с токеном /pay/<token>.

Документ основан на аудите кода от 20.10.2026: frontend/src/pages/trainingscreen/*, components/IndoorTV/*, pages/myClubs/sales/{pos,cash}/*, components/sales/*, backend/app/modules/{sales,billing,commerce,memberships,network,devices}/*, backend/app/modules/club/services/indoor_tv/*, tailwind.config.cjs, css/app.css.