общее
Всё, что не принадлежит одному разделу приложения: сначала полный граф переходов между всеми экранами, затем один узнаваемый угол шапки на каждом верхнеуровневом экране, затем точка перелома, где широкий экран возвращает форму консоли.
В Ago.Chat.* нет ни FCM, ни таблицы токенов устройств, ни APNs. Консоль оповещает через Notification API браузера при открытой странице. При буквальном портировании приложение будет уведомлять только на переднем плане — хуже, чем десктопная консоль, и ровно наоборот тому единственному, в чём нативное приложение незаменимо.
Без push всё остальное здесь делает приложение удобнее консоли, но не делает его нужным. Это изменение бэкенда, оно проектируется отдельной задачей — но экраны здесь нарисованы для мира, в котором push уже есть.
Две настоящие оговорки, которые остались оговорками: у Keycloak один публичный клиент и API проверяет одну аудиторию (лечится в ago-deploy); описания API в формате OpenAPI нет нигде (типы для передачи данных пишутся руками — ровно так же, как это уже делают консоль и виджет).
Каждый экран приложения — узел, каждое настоящее действие навигации — подписанная стрелка. Это первое, что стоит проверить: если форма здесь неверна, всё дальше можно не читать. Пять полос: до сессии, затем четыре раздела нижней навигации, затем «Ещё».
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
«Конфигурация записей» — единственный новый узел, и это единственный экран в приложении, вся работа которого — назвать порядок. Всё, что под ним, было достижимо и в консоли; недостижимой была фраза «вот эти шесть вещей, вот в таком порядке».
Из раздела «Записи» больше ничто не уходит в браузер. Пересчёт расписания мастера был единственной стрелкой во всём документе, открывавшей консоль; теперь это три шага внутри приложения.
Раньше правый угол шапки был разным на каждом экране: где-то зелёный кружок присутствия и «⋮», где-то только кружок, где-то вообще ничего. Теперь один и тот же элемент — инициалы оператора с индикатором присутствия внахлёст, со свечением — на всех пяти вкладках нижней навигации, и он же единственная дверь к «Настройки» и «Выйти». Экраны, куда попадают вглубь (тред, сами «Настройки»), используют другой, более простой вид шапки — стрелка назад и заголовок, без аватара: тот же принцип, что уже действует в шапке треда.
Один пункт навигации — «Настройки» — и «Выйти». Ни тема, ни язык не показаны прямо здесь: оба уже есть строками на экране «Настройки» рядом, и дублировать их в двух местах значит рано или поздно рассинхронизировать интерфейс с самим собой.
Экран «Настройки», куда ведёт пункт меню выше, живёт на странице «Настройки» — тема, язык интерфейса и текущий сайт.
Одна точка перелома, тот же код. Планшет — приятное дополнение, а не цель: усилий сюда вложено ровно столько, чтобы показать, что перелом существует.
Это утверждение стоит проверять тщательнее всего: если экран телефона построен как «два экрана и шторка», а не как уменьшенная сетка, то широкий layout — это точка перелома, а не вторая реализация. Это же причина, почему контекст посетителя — шторка, а не отдельный маршрут: маршруту некуда превращаться в панель.
На планшете возвращается собственная форма консоли: list-detail-каркас с контекстом посетителя как третьей панелью, а нижняя панель становится боковой навигационной. Тот же код, одна точка перелома — последний кадр на этой странице. Планшет — приятное дополнение, а не цель: телефон основной инструмент, и ни один этап плана не завязан на широкий экран.