# InfCRM Mobile — UX-мокапы

Интерактивная карта первой версии клиентского приложения для фитнес-клубов. Она переводит решения из [`mobile-client-app-development-plan.md`](../mobile-client-app-development-plan.md) в конкретные мобильные экраны и переходы.

## Как открыть

Откройте [`index.html`](./index.html) в браузере. Галерею можно фильтровать по сценарию, искать по названию или route и открывать каждый экран в увеличенном виде.

Ссылки в футере открывают README, UX-flow и design system в отдельном popup. Markdown отображается с таблицами, кодом, оглавлением и Mermaid-диаграммами. При открытии через обычный web server документы читаются напрямую; для автономного `file://`-режима после изменения `.md` выполните `node build-docs-content.mjs`, чтобы обновить [`docs-content.js`](./docs-content.js).

Это редактируемый UX-концепт, а не готовый React Native-код и не окончательная визуальная тема клуба. Макеты фиксируют информационную архитектуру, состояния и удобные действия; цвета, логотип и часть контента будут приходить из branding configuration выбранного клуба.

## Что уже спроектировано

| № | Экран | Route | Зачем нужен |
|---:|---|---|---|
| 01 | Первый вход | `/sign-in` | Только Google на Android или только Apple на iOS |
| 02 | Номер телефона | `/phone` | Международный номер целиком; флаг определяется по введённому +коду без picker |
| 03 | Поиск клуба или сети | `/clubs` | Compact-логотипы знакомых клубов с initials fallback и поиск организации после регистрации |
| 04 | Выбор филиала | `/branch-select` | Явный выбор филиала внутри сети |
| 05 | Способ подключения | `/connect-card` | Приглашение, проверочные данные или гостевой профиль |
| 06 | Код приглашения | `/invitation` | Ввод кода либо сканирование QR; текущий телефон показан read-only |
| 07 | Поиск карточки по данным | `/legacy-proof` | Read-only телефон из профиля + идентификатор CRM + сумма последней оплаты |
| 08 | Гостевой профиль | `/guest` | ФИО, дата рождения и read-only телефон пользователя из mobile-профиля |
| 09 | Карточка подключена | `/link-success` | Понятное завершение onboarding |
| 10 | Главная | `/:clubId` | Club cover, ближайшая запись, абонементы и быстрые действия |
| 11 | Список абонементов | `/memberships` | Optional product artwork и все абонементы выбранного филиала |
| 12 | Детали абонемента | `/membership/:id` | Artwork вида абонемента, остатки, срок и связанные правила |
| 13 | Общая карта клиента | `/card` | Штрихкод уровня клиента на настраиваемом card artwork |
| 14 | Выбор кода | `/card?select=1` | Переключение, если у абонементов разные значения карты |
| 15 | Общий код нескольких абонементов | `/card/:cardId` | Группировка одинакового значения без дублирования карт |
| 16 | Карта не настроена | `/card` | Безопасное состояние без выдуманного fallback-кода |
| 17 | Расписание | `/schedule` | Дата, фильтры, доступность и быстрый обзор занятий |
| 18 | Детали занятия | `/session/:id` | Вся информация и правила до записи |
| 19 | Подтверждение записи | `/session/:id/book` | Выбор подходящего абонемента и подтверждение |
| 20 | Запись без абонемента | `/session/:id/book` | Честное объяснение, когда применимый абонемент не найден |
| 21 | Запись подтверждена | `/booking-success` | Результат, следующий шаг и добавление в системный календарь |
| 22 | Мои записи | `/bookings` | Будущие, group waitlist и история |
| 23 | Детали записи | `/booking/:id` | Данные записи и самостоятельная отмена |
| 24 | Поздняя отмена | `/booking/:id` | Блокировка после club-configured лимита в часах |
| 25 | Вступление в лист ожидания | `/session/:id/waitlist` | Очередь только группового занятия: предварительная позиция и подтверждение вступления |
| 26 | Выбор тренера и времени | `/trainer-booking` | Начало сценария персональной тренировки |
| 27 | Переключатель контекста | `/club-switcher` | Сеть → филиал → одна карточка клиента |
| 28 | Управление карточками | `/card-management` | Подключённые филиалы и их статус |
| 29 | Замена карточки | `/replace-link` | Контролируемая замена связи в одном филиале |
| 30 | Клуб не найден | `/clubs` | Понятный пустой результат поиска клуба или сети |
| 31 | Профиль | `/profile` | Личные данные, настройки и будущие роли |
| 32 | Удаление аккаунта | `/account-deletion` | Обращение в поддержку/клуб без self-service удаления в v1 |
| 33 | Гость ожидает подтверждения | `/home` | Ограниченный режим до подтверждения администратором |
| 34 | Нет сети | `/home` | Последние сохранённые данные с явной отметкой времени |
| 35 | Лимит попыток | `/legacy-proof` | Блокировка после 10 попыток за сутки |
| 36 | Клуб недоступен | `/home` | Ошибка интеграции без потери уже сохранённого контекста |
| 37 | Пустое расписание | `/schedule` | Полезное пустое состояние с изменением фильтров |
| 38 | Сессия истекла | `/session-expired` | Повторная авторизация без двусмысленной ошибки |
| 39 | Подключение изменилось | `/context-conflict` | Безопасное восстановление после изменения карточек |
| 40 | Загрузка главной | `/home` | Skeleton без скачков компоновки |
| 41 | Сканирование QR-приглашения | `/invitation/scan` | Camera-only scanner после явного действия |
| 42 | Нет доступа к камере | `/invitation/scan` | Системные настройки и ручной код без блокировки linking |
| 43 | Карточка не подключена | `/link-error` | Linking errors без раскрытия телефона или клиента |
| 44 | Лист ожидания требует подтверждения | `/booking/:id` | Guest waitlist approval без резервирования места и самостоятельная отмена заявки |
| 45 | Выбор языка | `/settings/language` | Русский, английский и украинский с английским fallback |
| 46 | Изменение номера телефона | `/settings/phone` | Impact preview и отвязка всех карточек после подтверждения |
| 47 | Статус удаления аккаунта | `/account-deletion` | Срок обработки и отмена support-mediated заявки |
| 48 | Карта временно недоступна | `/card` | Безопасное `unavailable` отдельно от `not_configured` |
| 49 | Обновлённые условия | `/legal/acceptance` | Повторное принятие точной новой версии legal-документов |
| 50 | Требуется обновление | `/update-required` | Pre-login blocking state для отдельной minimum version/build policy Android или iOS |
| 51 | Абонементов пока нет | `/memberships` | Подключённый клиент без активных абонементов |
| 52 | Отвязка карточки | `/card-management/:linkId/unlink` | Подтверждение и объяснение сохранения CRM-данных |
| 53 | Место уже занято | `/session/:id/book` | Recovery после `409` за последнее место |
| 54 | Активные сессии | `/settings/sessions` | Просмотр и отзыв server sessions |
| 55 | Первый вход на iPhone | `/sign-in` | Только Sign in with Apple на iOS |
| 56 | Условия при первом входе | `/legal/acceptance` | Явное принятие Terms и Privacy после provider login |
| 57 | Юридический документ | `/legal/documents/:type` | Просмотр локализованной версии Terms или Privacy |
| 58 | Ошибка запуска | `/startup-error` | Recovery, когда bootstrap недоступен и локального кэша ещё нет |
| 59 | Помощь и контакты | `/help` | Телефон, email, адрес, часы работы и системная карта выбранного филиала |
| 60 | О приложении | `/about` | Версия, Terms, Privacy и лицензии |
| 61 | Выход из аккаунта | `/profile/logout` | Подтверждение logout без удаления клубных данных |
| 62 | Активный лист ожидания | `/booking/:id` | Фактическая позиция в очереди группового занятия и выход из неё |
| 63 | Выход из листа ожидания | `/booking/:id/leave-waitlist` | Подтверждение потери текущей позиции в очереди группового занятия |
| 64 | Подтверждение записи к тренеру | `/trainer-booking/confirm` | Проверка тренера, слота, услуги и абонемента |
| 65 | Тренировка забронирована | `/trainer-booking/success` | Подтверждение и добавление в календарь |
| 66 | Детали записи к тренеру | `/trainer-booking/:id` | Просмотр и самостоятельная отмена индивидуальной записи |
| 67 | Доступность абонемента по дням | `/membership/:id/availability` | Календарь действия с разрывами и заморозками для выбранного года |

## Основные решения интерфейса

- Сначала вход, принятие legal-документов и ввод телефона; только после этого начинается поиск клуба. Условие 18+ находится внутри обязательных Terms и не создаёт отдельного экрана.
- Пользователь вводит полный телефон вместе с `+кодом` страны. Флаг определяется автоматически по phone metadata; отдельного списка стран нет.
- В invitation/QR/legacy-proof/guest/replacement сценариях телефон нельзя изменить inline. Он только отображается из профиля, отсутствует в request DTO и подставляется BFF; смена номера выполняется отдельным действием в профиле.
- Вариант «Я ещё не клиент» и route `/guest` существуют только при effective `guestProfileCreation`. При выключении глобального флага клуба новые гости не создаются, но уже созданные guest-профили и записи сохраняются.
- Изображения имеют конкретные роли: global club cover — только home hero, client-card artwork — только фон цифровой карты, artwork вида абонемента — home/list/details, существующее фото сотрудника — trainer surfaces. Рекламной карусели и картинок каждого занятия в v1 нет.
- В каталоге клубов используется `logo-compact`, если он загружен. Отсутствующий или повреждённый логотип не создаёт пустую рамку: строка остаётся доступной и показывает инициалы клуба.
- В v1 интерфейс доступен на русском (`ru`), английском (`en`) и украинском (`uk`); английский используется как fallback.
- В v1 нет уведомлений: не показываются экран, колокольчик, badges и запрос разрешения на push.
- Выбранные клуб, филиал и карточка клиента всегда различимы. Приложение не смешивает данные разных филиалов.
- На один `(UserId, ConnectionId, ClubId)` активна одна карточка `RemoteClientId`. Пользователь может подключить карточки в нескольких филиалах одной сети, но не несколько людей в одном филиале.
- Абонементы не являются отдельными подключаемыми аккаунтами. После подключения карточки показываются все её абонементы, сгруппированные по состоянию.
- Список и детали показывают простой общий срок `min–max`. Для календарных абонементов отдельное действие открывает экран 67 с точной доступностью по дням; у одноразовых абонементов без срока такого действия нет.
- Карта доступна с главной и из постоянной навигации за одно-два нажатия.
- Значение штрихкода приходит от настроенного CRM-провайдера. UI не показывает внутренние идентификаторы и не подставляет другое поле без решения CRM.
- Этот provider относится только к mobile card display и mobile legacy linking. Он не меняет существующий поиск клиента, check-in, terminal/public lookup или другие сценарии админки CRM.
- Перед записью и отменой показывается эффект действия: используемый абонемент, списание, лимит отмены, ожидание или ограничение гостя.
- Лист ожидания существует только для групповых занятий. У записи к тренеру нет очереди, позиции или статуса `Waitlisted`.
- Если гостевая заявка требует решения клуба, она сразу попадает в group waitlist без резервирования места. До approval позиция не показывается; после решения заявка либо подтверждается при доступной очереди/вместимости, либо остаётся обычной одобренной строкой waitlist.
- После успешной групповой или персональной записи можно открыть системную форму добавления события в календарь.
- Все пользовательские обращения направляются в выбранный клуб через его телефон или email; отдельный публичный канал InfCRM в приложении не показывается.
- Любое недоступное действие объясняет причину и предлагает следующий реальный шаг.

## Структура папки

```text
docs/mobile-app/
├── index.html                 # интерактивная галерея
├── app.js                     # данные и разметка 67 экранов
├── styles.css                 # mobile UI kit и gallery layout
├── UX-FLOWS.md                # переходы и продуктовые сценарии
├── SCREEN-COVERAGE.md         # сверка v1-сценариев с layout и вариантами состояний
├── DESIGN-SYSTEM.md           # токены, branding и accessibility
├── build-docs-content.mjs     # обновление Markdown для автономного file:// режима
├── docs-content.js            # сгенерированное содержимое документов
├── verify-docs-popup.mjs      # browser-проверка Markdown и Mermaid popup
├── render-mockups.mjs         # локальный экспорт PNG через Playwright
└── assets/                    # локальные визуальные ресурсы
```

Сгенерированные PNG из `previews/` не хранятся в репозитории. При необходимости их можно локально пересобрать командой `node render-mockups.mjs`; папка с результатом игнорируется Git.

Файлы `sample-club-cover-web.webp`, `sample-client-card-artwork-web.webp` и `sample-membership-artwork-web.webp` — оптимизированные сгенерированные демонстрационные assets только для прототипа. В реальном приложении их заменяют изображения, загруженные клубом и полученные через semantic BFF media routes.

## Деплой в Azure Static Web Apps

Azure DevOps pipeline находится в `cicd/mobile-app-static-web-app.yml`. Он использует тот же self-hosted pool `Default`, что и основной build, запускается при изменениях этой папки в `main`, формирует минимальный статический пакет без `previews/` и `test-assets/` и публикует его через Azure Static Web Apps CLI без Oryx-сборки.

Перед первым запуском:

1. Создайте ресурс Azure Static Web App.
2. Скопируйте его deployment token: Azure Portal → Static Web App → Overview → Manage deployment token.
3. Создайте pipeline из существующего YAML `cicd/mobile-app-static-web-app.yml`.
4. Добавьте секретную pipeline variable `AZURE_STATIC_WEB_APPS_API_TOKEN` со скопированным токеном.

На Azure-хосте автоматически открывается prototype-режим. Расширенная галерея остаётся доступна по адресу `?isDev=true`.

## Как обсуждать и менять

В комментарии достаточно сослаться на номер экрана, например: «на 19 перед подтверждением показать правило списания». Экранные данные находятся в массиве `screens` в [`app.js`](./app.js), а общие компоненты и токены — в начале [`app.js`](./app.js) и [`styles.css`](./styles.css).

Перед переносом в Expo нужно подтвердить тексты, список серверных capabilities и фактические поля API. Имена route в макете концептуальные: они задают навигационную модель, но не фиксируют конкретную библиотеку роутинга.
