общее

Общее — граф переходов, шапка, широкий экран

Всё, что не принадлежит одному разделу приложения: сначала полный граф переходов между всеми экранами, затем один узнаваемый угол шапки на каждом верхнеуровневом экране, затем точка перелома, где широкий экран возвращает форму консоли.

54маршрута консоли изучено, один за другим, по App.tsx и consoleNav.ts
49входят в объём Android — 19 как есть, 29 с редизайном, 1 как механизм
5исключены — только экраны владельца платформы. Пересчёт расписания мастера вернулся в объём
6экранов без аналога в консоли, включая конфигурацию записей

Push — это не оговорка к приложению, это то, ради чего оно существует

В Ago.Chat.* нет ни FCM, ни таблицы токенов устройств, ни APNs. Консоль оповещает через Notification API браузера при открытой странице. При буквальном портировании приложение будет уведомлять только на переднем плане — хуже, чем десктопная консоль, и ровно наоборот тому единственному, в чём нативное приложение незаменимо.

Без push всё остальное здесь делает приложение удобнее консоли, но не делает его нужным. Это изменение бэкенда, оно проектируется отдельной задачей — но экраны здесь нарисованы для мира, в котором push уже есть.

Две настоящие оговорки, которые остались оговорками: у Keycloak один публичный клиент и API проверяет одну аудиторию (лечится в ago-deploy); описания API в формате OpenAPI нет нигде (типы для передачи данных пишутся руками — ровно так же, как это уже делают консоль и виджет).

00

Граф переходов: откуда куда можно попасть

Каждый экран приложения — узел, каждое настоящее действие навигации — подписанная стрелка. Это первое, что стоит проверить: если форма здесь неверна, всё дальше можно не читать. Пять полос: до сессии, затем четыре раздела нижней навигации, затем «Ещё».

flowchart TD
  classDef out fill:#f6f5f1,stroke:#c8c3b6,color:#57546f;
  classDef entry fill:#ecebff,stroke:#4b3aff,color:#3324c9;
  classDef hub fill:#fff4e0,stroke:#7a4d00,color:#7a4d00;

  Launch(["Запуск"]):::entry --> HasSession{"Есть сессия?"}
  HasSession -- "нет" --> SignIn["Вход"]
  HasSession -- "да" --> Router
  SignIn -- "Войти" --> KC["Keycloak"]:::out
  SignIn -- "Создать аккаунт" --> Onboard["Регистрация сайта"]
  SignIn -- "Соглашение" --> Policy["Документ политики"]
  KC --> Router{"Маршрутизация после входа"}
  Router -- "есть место оператора" --> ConvList
  Router -- "несколько тенантов" --> SitePick["Выбор сайта"]
  SitePick --> ConvList
  Router -- "только владелец платформы" --> OwnerStop["Владелец платформы: только веб"]
  Router -- "ни то ни другое" --> Onboard
  OwnerStop -. "Открыть в браузере" .-> KCWeb["Веб-консоль"]:::out
  Onboard --> ConvList

  InviteLink(["App Link приглашения"]):::entry -. "открыт по ссылке" .-> Invite["Приглашение"]
  Invite -- "Принять" --> Redeem["Ввод кода приглашения"]
  Redeem --> Router
  PolicyLink(["Ссылка на политику"]):::entry -. "без сессии" .-> Policy

  PushMsg(["Push: новое сообщение"]):::entry -. "тап" .-> Thread
  PushBook(["Push: запись ждёт решения"]):::entry -. "тап" .-> BookingCard
  ConvLink(["App Link диалога"]):::entry -. "тап" .-> Thread

  subgraph D["1 · Диалоги"]
    direction TB
    ConvList["Список диалогов · Мои / Ожидают / Все (site:configure)"]
    Thread["Тред"]
    Sheet["Шторка посетителя"]
    Notes["Заметки команды"]
    Contacts["Контактные данные"]
    History["Прошлые диалоги"]
    Ident["Связанные каналы"]
    TagSheet["Шторка меток"]
    Canned["Готовые ответы"]
    CloseSheet["Итог закрытия"]
    Search["Поиск по диалогам"]
    Restricted["Ограниченные посетители"]
  end

  ConvList -- "тап по строке" --> Thread
  ConvList -- "чип фильтра" --> TagSheet
  ConvList -- "лупа" --> Search
  ConvList -- "⋮ · site:configure" --> Restricted
  Search -- "тап по находке" --> Thread
  Thread -- "назад" --> ConvList
  Thread -- "чип посетителя" --> Sheet
  Thread -. "скрепка · если разрешены вложения" .-> Picker["Системный выбор файла"]:::out
  Thread -- "/ в начале строки" --> Canned
  Thread -- "⋮ Закрыть" --> CloseSheet
  CloseSheet -- "итог выбран" --> ConvList
  Sheet -- "Заметки" --> Notes
  Sheet -- "Контакты" --> Contacts
  Sheet -- "Прошлые" --> History
  Sheet -- "Каналы" --> Ident
  Sheet -- "метка" --> TagSheet
  History -- "тап" --> Thread
  Contacts -- "Показать · под аудитом" --> Contacts

  subgraph Z["2 · Записи"]
    direction TB
    Pending["Ожидают"]
    Confirmed["Утверждены"]
    Clients["Клиенты"]
    BookingCard["Карточка записи"]
    ClientCard["Карточка клиента"]
    Merge["Объединение клиентов"]
    Cfg["Конфигурация записей"]:::hub
    Cal["Календари"]
    Services["Услуги"]
    Workers["Мастера"]
    WorkerCard["Карточка мастера"]
    Schedule["График мастера"]
    Slots["Слоты мастера"]
    Recut1["Пересчёт · шаг 1: дата"]
    Recut2["Пересчёт · шаг 2: решения"]
    Recut3["Пересчёт · шаг 3: подтверждение"]
    Exceptions["Исключения: выходной и короткий день"]
    Origins["Разрешённые источники"]
    MergeLog["Журнал объединений"]
  end

  Pending -- "сегмент" --> Confirmed
  Confirmed -- "сегмент" --> Clients
  Clients -- "сегмент" --> Pending
  Pending -- "тап" --> BookingCard
  Pending -- "Подтвердить / Отклонить" --> Pending
  Confirmed -- "лента дат" --> Confirmed
  Confirmed -- "тап" --> BookingCard
  Clients -- "тап" --> ClientCard
  ClientCard -- "Объединить" --> Merge
  Merge -- "необратимо, после подтверждения" --> Clients
  BookingCard -- "Показать телефон · под аудитом" --> BookingCard

  Pending -- "⋮ Конфигурация" --> Cfg
  Cfg -- "Не хватает · Исправить" --> Cal
  Cfg -- "Не хватает · Исправить" --> Workers
  Cfg -- "Не хватает · Исправить" --> Services
  Cfg -- "Не хватает · Исправить" --> Schedule
  Cfg -- "Посмотреть слоты" --> Slots
  Cfg --> Exceptions
  Cfg --> Origins
  Cfg --> MergeLog
  Workers -- "тап" --> WorkerCard
  Workers -- "добавить" --> WorkerCard
  WorkerCard -- "Когда работает" --> Schedule
  WorkerCard -- "Что получилось" --> Slots
  WorkerCard -- "Что делает" --> Services
  Schedule -- "сохранить" --> WorkerCard
  Schedule -- "Пересчитать уже нарезанное" --> Recut1
  Slots -- "Пересчитать" --> Recut1
  Recut1 -- "Показать, что изменится" --> Recut2
  Recut2 -- "все решения приняты" --> Recut3
  Recut3 -- "выполнено" --> Slots

  subgraph T["3 · Команда"]
    direction TB
    TeamChat["Общение"]
    People["Люди"]
    InviteSheet["Приглашение оператора"]
  end

  TeamChat -- "сегмент · site:manage_operators" --> People
  People -- "Пригласить" --> InviteSheet
  InviteSheet -. "Поделиться" .-> Share["Системная шторка Поделиться"]:::out

  subgraph A["4 · Аналитика"]
    direction TB
    MyNumbers["Мои показатели"]
    SiteStats["По сайту"]
    Conversion["Конверсия"]
    TagStats["По меткам"]
    Funnel["Воронка записи"]
    Reveals["Показы телефонов"]
  end

  MyNumbers -- "⋮ · site:configure" --> SiteStats
  MyNumbers -- "⋮ · site:configure" --> Conversion
  MyNumbers -- "⋮ · site:configure" --> TagStats
  MyNumbers -- "⋮ · site:configure" --> Funnel
  MyNumbers -- "⋮ · calendar:configure" --> Reveals

  subgraph M["5 · Ещё"]
    direction TB
    More["Ещё"]
    Install["Установка виджета"]
    WidgetCfg["Виджет на сайте"]
    ChMax["MAX"]
    ChTg["Telegram"]
    ChVk["VK"]
    ChMail["Почта"]
    CannedCfg["Готовые ответы"]
    AiSug["ИИ-подсказки"]
    AutoReply["Автоответ вне смены"]
    Faq["База знаний"]
    TagsCfg["Метки"]
    Products["Продукты"]
    Billing["Оплата"]
    Docs["Документы"]
    AiCfg["ИИ и данные"]
    Storage["Хранилище"]
    Export["Выгрузка"]
    Settings["Настройки"]
    Theme["Тема"]
    Notif["Уведомления"]
    SiteSwitch["Текущий сайт"]
    DeleteAcc["Удалить аккаунт"]
  end

  More --> Install
  More --> WidgetCfg
  More --> ChMax
  More --> ChTg
  More --> ChVk
  More --> ChMail
  More --> CannedCfg
  More --> AiSug
  More --> AutoReply
  More --> Faq
  More --> TagsCfg
  More --> Products
  More --> Billing
  More --> Docs
  More --> AiCfg
  More --> Storage
  More --> Export
  More --> Settings
  Settings --> Theme
  Settings --> Notif
  Settings --> SiteSwitch
  Settings --> DeleteAcc
  Settings -- "Выход" --> SignIn
  SiteSwitch -- "другой сайт" --> ConvList
  Billing -. "Оформить оплату" .-> Checkout["Оплата в веб-консоли"]:::out
  Export -. "Скачать архив" .-> Dl["Системный загрузчик"]:::out
  Storage -- "долгое нажатие" --> Storage

  ConvList <-- "нижняя навигация" --> Pending
  Pending <-- "нижняя навигация" --> TeamChat
  TeamChat <-- "нижняя навигация" --> MyNumbers
  MyNumbers <-- "нижняя навигация" --> More
обычный переход внутри приложения вход снаружи: deep link или тап по уведомлению уходит из приложения: Custom Tab, системный выбор файла, «Поделиться»

Две вещи, которые граф утверждает и которые легко пропустить

«Конфигурация записей» — единственный новый узел, и это единственный экран в приложении, вся работа которого — назвать порядок. Всё, что под ним, было достижимо и в консоли; недостижимой была фраза «вот эти шесть вещей, вот в таком порядке».

Из раздела «Записи» больше ничто не уходит в браузер. Пересчёт расписания мастера был единственной стрелкой во всём документе, открывавшей консоль; теперь это три шага внутри приложения.

08

Аккаунт и шапка — один узнаваемый угол на каждом верхнеуровневом экране

Раньше правый угол шапки был разным на каждом экране: где-то зелёный кружок присутствия и «⋮», где-то только кружок, где-то вообще ничего. Теперь один и тот же элемент — инициалы оператора с индикатором присутствия внахлёст, со свечением — на всех пяти вкладках нижней навигации, и он же единственная дверь к «Настройки» и «Выйти». Экраны, куда попадают вглубь (тред, сами «Настройки»), используют другой, более простой вид шапки — стрелка назад и заголовок, без аватара: тот же принцип, что уже действует в шапке треда.

9:41
Диалоги
АГ
АГ
Мария Петрова
m.petrova@example.com
Настройки
Выйти
меню профиля · в духе GitHub — без лишних органов управления внутри

Один пункт навигации — «Настройки» — и «Выйти». Ни тема, ни язык не показаны прямо здесь: оба уже есть строками на экране «Настройки» рядом, и дублировать их в двух местах значит рано или поздно рассинхронизировать интерфейс с самим собой.

Экран «Настройки», куда ведёт пункт меню выше, живёт на странице «Настройки» — тема, язык интерфейса и текущий сайт.

07

Широкий экран возвращает форму консоли

Одна точка перелома, тот же код. Планшет — приятное дополнение, а не цель: усилий сюда вложено ровно столько, чтобы показать, что перелом существует.

9:41
Диалоги
Записи
Команда
Аналитика
Ещё
Мои · 3
🦊🍊
Лиса · Апельсин2 мин
Оплата не прошла
2
🐳🍕
Ирина С.14 мин
Спасибо!
🦉🍓
Сова · Клубника1 ч
Перенести на пятницу?
1
Лиса · Апельсин · виджет
Здравствуйте! Оплата не прошла, а деньги списались.09:38
Добрый день! Сейчас посмотрю по заказу.09:39 ✓✓
Спасибо09:40
Сообщение…
Контекст
Оплата
Срочно
⚠️ ТелефонПоказать
ПочтаПоказать
Заметки2
КаналыTG
ListDetailPaneScaffold · те же три зоны, что и в консоли

Это утверждение стоит проверять тщательнее всего: если экран телефона построен как «два экрана и шторка», а не как уменьшенная сетка, то широкий layout — это точка перелома, а не вторая реализация. Это же причина, почему контекст посетителя — шторка, а не отдельный маршрут: маршруту некуда превращаться в панель.

Что возвращается на планшете

На планшете возвращается собственная форма консоли: list-detail-каркас с контекстом посетителя как третьей панелью, а нижняя панель становится боковой навигационной. Тот же код, одна точка перелома — последний кадр на этой странице. Планшет — приятное дополнение, а не цель: телефон основной инструмент, и ни один этап плана не завязан на широкий экран.