TRIDE Sales Screen — витрина и точка продаж на экране устройства
Новый режим экрана для планшетов и TV клуба: в простое — красивая слайд-витрина продуктов, по тапу сотрудника — полноэкранная касса (POS) с продажами, записью на занятия и оплатой, в том числе QR-кодом прямо на экране. Всё строится на переиспользовании существующего стека продаж, касс и Indoor TV.
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, выдача самовывоза по QRtride://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.Цели, метрики, не-цели
Не-цели (осознанно не делаем в этом плане): полноценный киоск самообслуживания с покупкой без сотрудника (кандидат в Ф3), новая фискализация или платёжный провайдер, автоматическая печать на Bluetooth-принтерах, изменения в модель членств/скидок.
4.Обзор решения: два состояния одного экрана
Устройство (планшет на стойке, TV, Tridebox) переводится в новый режим — «Экран продаж» (devices.pos_mode, включается в админке). Для него маршрут /screen вместо тренировочного контура рендерит новый диспетчер PosMode.svelte с конечным автоматом из четырёх состояний:
Переходы состояний локальные (без перезагрузки), с плавной анимацией. Из витрины по тапу не попадают в приложение напрямую: экран входа кассира — единственная дверь, что закрывает и приватность, и атрибуцию продаж.
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.
купите со смартфона
tv_anouncements), индикатор прогресса слайда. Тап в любом месте — вход для кассира (подсказка «Кассир: коснитесь экрана» выводится мягко мигающим лейблом первые 10 с после смены слайда).UpcomingSchedule. QR ведёт на полный каталог покупки. Подходит для «дежурного» слайда между hero-слайдами.Промо-слайд (Ф2)
Ручной слайд-конструктор в админке: заголовок, текст, фон (фото/градиент), бейдж акции, дата окончания («осталось N дней»), QR на покупку или лид-форму. Промо-слайды вставляются в ротацию с заданной частотой (например, каждый 3-й слайд).
5.3. Переходы, эффекты, ритм
- Длительность слайда: 8–30 с (по умолчанию 12 с), отдельно для hero/прайса; прогресс-полоса как в
OverviewReel(CSSanimation … 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.
IndoorTVPreviewModal рендерит настоящий ScheduleMode). Кнопка «Проверить на устройстве» отправляет обновление и показывает статус доставки.5.5. Витрина внутри обычного Indoor TV
Для больших телевизоров, уже крутящих эфир: новый тип трека products в tv_playlists. Трек несёт конфиг витрины (категории/макет/длительность) и рендерится в main-слоте лэйаута тем же ShowcasePlayer. Так «продажи» попадают в общую ротацию клуба (видео → продукты → рейтинг → …) без нового расписания. Показы трека считаются в существующей аналитике Indoor TV с детализацией по продуктам.
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.*идут с ним. Продавец, права и журнал продаж получаются «бесплатно», транспорт не меняется.
обновляется автоматически
confirmScreenAuthRequest): никаких новых паролей и выдач, личность и права берутся из сессии сотрудника. Резервный PIN-вход — опция на устройство (по умолчанию выключена). 30 с бездействия на этом экране — авто-возврат в витрину.6.2. POS-дашборд
Ядро — существующие PosCatalog + SaleReceipt (режим inline, источник продажи pos); новый компонент-обвязка добавляет киоск-хедер, крупные тач-таргеты и быстрые действия. Раскладка — как у кассы: каталог слева (3fr), чек справа (2fr); на вертикальном планшете — вкладки.
/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 на экране планшета — главный новый сценарий:
/pay/<token>→
клиент сканирует камерой→
оплата на смартфоне (СБП/карта/виджет)→
поллинг 5 с→
экран успеха
- QR-экран — киоск-вариант
SaleLinkResult: крупный QR, сумма, таймер жизни ссылки, статус «ожидаем оплату…», кнопки «Отправить ссылку» (push/SMS/чат —sales.offer.send) и «Отмена». - Клиент по ссылке попадает на привычный
/pay/:token: виджет ЮKassa (включая QR СБП), CloudPayments, Яндекс Пэй — без новых платёжных интеграций. - Таймаут бездействия во время активного поллинга не срабатывает; счёт unpaid по истечении TTL гаснет, чек возвращается в дашборд.
/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_screencdevice_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-аналитика «показ → продажа». | отдельный план | — |
club_kkt/ручными чеками. ③ Разнообразие Android-планшетов: wake-по-тапу и nosleep проверяем на целевых моделях. ④ Приватность: авто-возврат — обязательное требование приёмки, а не «желание».11.Открытые вопросы (для согласования)
- Идентификация кассира: предложено — QR из приложения сотрудника как основной (без новых креденшелов), PIN как выключенная по умолчанию опция. Нужен ли вообще PIN-фолбэк в Ф1, или отложить до запроса клубов? Альтернатива на будущее — карта/браслет сотрудника.
- Показ цен на витрине — по умолчанию включать или скрывать (часть клубов не афиширует прайс)? Предложение: дефолт «скрыть», включается на устройстве.
- QR покупки на слайдах (клиент покупает сам): включаем в Ф1 или Ф2? Предложение: Ф2 — сначала привыкаем к показу.
- Таймаут по умолчанию — 5 минут подходит? Нужен ли ночной режим (планшет полностью гасит экран ночью)?
- Одна смена — много кассиров: нужна ли в Ф1 «общая смена стойки» или достаточно схемы «каждая продажа за своим кассиром»?
- Печать чеков: достаточно ли существующей фискализации клуба, или Bluetooth-принтер — реальное требование ресепшена (тогда это Ф3 с железом)?
- Место настроек в админке: «Продажи → Экран продаж» (предложение) или вкладка в Indoor TV?
- Витрина и эфир одновременно: нужен ли сценарию «табло у входа» отдельный лэйаут-слот витрины (маленькая колонка цен сбоку от видео)?
- Пилотные клубы: какие 2–3 клуба берём и на каком оборудовании (модели планшетов)?
12.Приложение
12.1. Карта переиспользования
| Возможность | Переиспользуем | Новое |
|---|---|---|
| Каталог плитками, фильтры, поиск | PosCatalog.svelte, sales.sellable.list | киоск-размеры |
| Чек: клиент, скидки, промо, позиции | SaleReceipt.svelte (inline, source pos), sales.preview | — |
| Оплата наличные/терминал | sales.offline.charge, кассы/смены | — |
| Оплата QR | sales.offer.create/issue/send, /pay/:token, поллинг SaleLinkResult | полноэкранный QR-экран |
| Запись на занятие | SaleSessionPicker, bookings.admin.create | кнопка «Записать» |
| Сканеры | createScanner, goods.find_by_code, tride://pickup | — |
| Витрина-слайдер | паттерны OverviewReel, UpcomingSchedule, Clock, AnnouncementPlayer | ShowcasePlayer + слайды |
| Аналитика показов | impressionsQueue → tv_impressions → statsmanager | разрез product×device |
| Режим устройства | регистрация/автологин устройства, getscreenmode, ScreenRecoveryService | pos_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.