docs: Time Arc v3 following and discover when. Refs EventHub/EventHubFront#52

This commit is contained in:
2026-08-13 18:16:07 +03:00
parent 951d070d63
commit bc1d770f16
+68 -40
View File
@@ -59,39 +59,50 @@ Deep-link на календарь, владельцем которого явл
`pending=0`; при `pending > 0` — badge со счётчиком (deep-link `/invites` работает). `pending=0`; при `pending > 0` — badge со счётчиком (deep-link `/invites` работает).
**Mood в Ещё нет** — chip в AppShell рядом с BrandWordmark (Front#40; ранее Front#39 **Mood в Ещё нет** — chip в AppShell рядом с BrandWordmark (Front#40; ранее Front#39
в chrome workspace). в chrome workspace).
- **Mood + Lens (Front#39/#40 + controls IA):** mood chip (FLOW/MOMENTUM/CONTROL → sheet picker) — - **Mood + Lens (Front#39/#40 + Front#46):** mood chip (FLOW/MOMENTUM/CONTROL → sheet picker) —
в **AppShell** рядом с логотипом (**не** в toolbar Month/Week/Day). Workspace chrome в **AppShell** рядом с логотипом (**не** в toolbar Month/Week/Day). **Lens** — инфо-табло
разнесён по зонам: (1) селектор календаря; (2) период + вид Месяц/Неделя/День; **под строкой AppShell** (logo/mood/nav) на calendar workspace: сегменты Обзор / Сегодня /
(3) owner schedule actions («+ Новое событие», «Заполнить расписание» — отдельный ряд); Заявки + метрики/chips (не рядом с ViewMode). Soft-default по mood **без auto-pin** при
(4) lens «Обзор / На сегодня / Заявки» — **в шапке** collapsible lens strip (underline/text клике на вкладку; при смене mood — `DEFAULT_LENS_BY_MOOD`. Workspace chrome: (1) селектор;
tabs на desktop, sheet на mobile; **не** рядом с ViewMode chips). Strip — Front-only derive (2) период + вид Месяц/Неделя/День; (3) owner actions — для commercial только
из уже загруженных events/bookings. Без flip календаря; без 4-го CREATOR. Strip по умолчанию «+ Новое событие» («Заполнить расписание» — во вкладке Студия). Без flip / CREATOR.
раскрыт на desktop, свёрнут на mobile. Нет на HTML-архиве месяца. Нет lens на HTML-архиве месяца / не-calendar routes.
- **Week view (Front#40):** одна строка day-headers (без дубля заголовков - **Week view (Front#40):** одна строка day-headers (без дубля заголовков
`WeekDayColumn`); горизонтальный скролл через `.eh-cal-week-scroll` (mobile ~390). `WeekDayColumn`); горизонтальный скролл через `.eh-cal-week-scroll` (mobile ~390).
- Контекст виджета: свой календарь (селектор) или чужой (browse после поиска). Чужой `personal` - Контекст виджета: свой календарь (селектор) или browse чужого (после поиска). Чужой `personal`
только просмотр; чужой `commercial` с `booking_open=true` — запись; при `booking_open=false` только просмотр; чужой `commercial` с `booking_open=true` — запись; при `booking_open=false`
(restricted / нет подписки владельца) — просмотр + сообщение «Запись временно недоступна». — просмотр + «Запись временно недоступна». Бейдж «чужой» **не** показывать; CTA
- **Свой default personal:** поверх сетки — overlay confirmed записей на чужие commercial **«Мой календарь»** + `ChevronLeft``/default`.
слоты (Front-only: `GET /v1/user/bookings` → events/calendars). Заголовок - **Единственный personal** (Back: title `Default`, UI «По умолчанию»): overlay confirmed
`событие · студия · специалист`; клик → `/c/:foreignCalId/e/:eventId` (не в своём cal). записей на чужие commercial + specialist duty (Front#35/#43) — **только** на нём, не на
Дополнительно (Front#43): overlay **своих** рабочих слотов commercial, где студиях. Create calendar UI — только тип **Студия** (`commercial`). Селектор: «По умолчанию»
`specialist_id` = текущий user — gate через accepted `GET /v1/user/specialist-invites` + студии.
+ проверка active в `GET …/specialists`, затем `GET …/events?from&to`; title
`событие · студия`. Personal владельца без accepted invite / без своих слотов
чужой roster **не** зеркалит.
- **Guest на чужом commercial (D9 модель B):** roster карточек активных специалистов → - **Guest на чужом commercial (D9 модель B):** roster карточек активных специалистов →
тап → сетка только со слотами `event.specialist_id` = выбранный; «Все слоты студии» тап → сетка только со слотами `event.specialist_id` = выбранный; «Все слоты студии»
снимает фильтр. Это **не** chip-filter на общей сетке. Book как обычно — без снимает фильтр. Это **не** chip-filter на общей сетке. Book как обычно — без
`specialist` в body заявки (слот уже привязан к спецу на событии). `specialist` в body заявки (слот уже привязан к спецу на событии).
- Agenda rail: empty copy зависит от типа календаря — personal «Нет событий», commercial В режиме «Все» — тот же **studio-агрегат** слотов, что у owner «Студия» (см. ниже);
«Нет слотов». фильтр D9 **без** второго UI режима.
- Выбор события открывает карточку действий: mobile — bottom sheet; desktop — боковая панель. Действия зависят от роли (owner / participant). - **Owner commercial workspace — режимы Студия / Мастер:**
- Без выбранного события — панель «О календаре» (описание, title/meta, рейтинг, отзывы). - **Студия** — агрегат слотов по `start_time` + `duration`: одна ячейка с заливкой
Форма отзыва на календарь/событие — только при confirmed booking; жалоба доступна без записи. `booked/total`, label `HH:mm · K/N свободно`; клик → popover мастеров со статусами
На чужом календаре — CTA **«Отслеживать» / Follow** (не путать с платной «Подписка»); (занятость по `booking_occupancy` / active bookings, BackSpec §6 events).
follow не открывает форму отзыва. - **Мастер** — фильтр сетки по `specialist_id`; sticky roster как guest D9 B
- На чужом `/c/:id` — CTA «Мой календарь» и таб Calendar не `aria-current` (intent `browse`). (**owner тоже** использует roster-фильтр, не только guest).
- **Месяц + Студия:** density markers в ячейках дня, **не** список чипов слотов.
- **Personal:** без studio-агрегата и без вкладки «Команда» / Team.
-`0.0` на free-слотах **не** показывать.
- Agenda / rail desktop (commercial owner): вкладки **Расписание | Команда | Студия** в
общем `.eh-owner-rail-panel` **фиксированной высоты** (вкладки не прыгают). Команда:
invite свёрнут за «Пригласить». **Студия** = hub: about + «Заполнить расписание» +
«Редактировать» (+ link `/calendars`); Delete только на `/calendars`. Mobile: segmented
**День | Команда | Студия** под grid. Списки — `ScrollRegion` (полоса скрыта, стрелки
по краям при overflow); dialogs — thin scrollbar on hover.
- Agenda empty: personal «Нет событий»; commercial «Свободных окон нет». UI-тип commercial —
«Студия» (не «коммерческий»).
- Выбор события открывает карточку действий: mobile — bottom sheet; desktop — боковая панель.
- На browse `/c/:id` — CTA «Мой календарь» (+ иконка) → `/default`; tab Calendar не
`aria-current`. Follow на чужом commercial без изменений.
- Список отслеживаемых: `/following` (из «Ещё»). - Список отслеживаемых: `/following` (из «Ещё»).
- Открытие события — `navigate` push (не `replace`), чтобы Back возвращал к календарю. - Открытие события — `navigate` push (не `replace`), чтобы Back возвращал к календарю.
- Owner: WS subscribe на активный календарь; `PUT` события из карточки; `PUT` календаря на `/calendars`. - Owner: WS subscribe на активный календарь; `PUT` события из карточки; `PUT` календаря на `/calendars`.
@@ -127,12 +138,10 @@ Deep-link на календарь, владельцем которого явл
Time Arc appicon — только favicon, не в switcher. Time Arc appicon — только favicon, не в switcher.
**Lens** (отдельно от mood и от view month/week/day): `overview` / `today` / `bookings` **Lens** (отдельно от mood и от view month/week/day): `overview` / `today` / `bookings`
(UI: Обзор / На сегодня / Заявки). Переключатель — в шапке lens strip (не в toolbar рядом (UI: Обзор / Сегодня / Заявки). Инфо-табло **под AppShell** на calendar routes (не в
с Месяц/Неделя/День). Persist только `localStorage` (`eh.calendar.lens` + toolbar ViewMode). Persist: session preference + soft LS; клик по вкладке **не** ставит
`eh.calendar.lens.pinned`); **не** поле профиля / Back API. Soft-default при смене mood, pin навсегда; при смене mood — soft-default FLOW→overview, MOMENTUM→today,
если lens не pinned пользователем: FLOW→overview, MOMENTUM→today, CONTROL→bookings. CONTROL→bookings. Контент: free gaps / события сегодня / upcoming bookings (Front-derive).
Strip над сеткой: free gaps сегодня / события сегодня / upcoming bookings (derive
на клиенте из уже загруженных данных).
Язык UI (`ru`/`en`): до логина — `navigator.language`; после входа — поле `language` профиля (`PATCH /v1/user/me`). Если в профиле язык пуст — при логине записывается текущий (браузерный/выбранный на экране входа). Язык также в `/more` и в форме `/profile`. Язык UI (`ru`/`en`): до логина — `navigator.language`; после входа — поле `language` профиля (`PATCH /v1/user/me`). Если в профиле язык пуст — при логине записывается текущий (браузерный/выбранный на экране входа). Язык также в `/more` и в форме `/profile`.
@@ -144,19 +153,23 @@ Strip над сеткой: free gaps сегодня / события сегод
**v2 (Front#51):** отказ от дуги (тап другой свободной ячейки / свайп недели) пишет штраф часу и мастеру в `localStorage` (`eh.timeArc.skips`) — дуга переезжает. ≥2 confirmed записей к одному `specialist_id` бустят его слоты. Месяц: тепло `data-time-arc-heat` на днях со скоренным часом. Personal: призрак «тот же час +7д» после прошедшего confirmed визита; тап открывает студию. **v2 (Front#51):** отказ от дуги (тап другой свободной ячейки / свайп недели) пишет штраф часу и мастеру в `localStorage` (`eh.timeArc.skips`) — дуга переезжает. ≥2 confirmed записей к одному `specialist_id` бустят его слоты. Месяц: тепло `data-time-arc-heat` на днях со скоренным часом. Personal: призрак «тот же час +7д» после прошедшего confirmed визита; тап открывает студию.
**v3 (дуга между календарями):** `/following` сортирует студии по ближайшему Time Arc; тап открывает неделю студии с курсором на этом дне (`data-time-arc` как в v1). Discover остаётся списком: на строке calendar — «когда» (`data-time-arc-when`), без общей сетки нескольких студий. Ghosts владельца считаются по активной студии (переключение календаря не смешивает паттерны). По-прежнему без LLM / Back AI.
## 4. Маршруты ## 4. Маршруты
Публичные: Публичные:
- `/login`, `/register`, `/verify` - `/login`, `/register`, `/verify`
Защищённые (`ProtectedRoute`): Защищённые (`ProtectedRoute`):
- `/` — Calendar Workspace (автовыбор своего календаря или empty-state) - `/default` — resolve единственного personal → `/c/:id` (post-login и tab «Календарь»)
- `/default/e/:eventId` — то же + карточка события
- `/` — alias → `/default`
- `/c/:calendarId` — workspace с календарём - `/c/:calendarId` — workspace с календарём
- `/c/:calendarId/e/:eventId` — workspace + карточка события - `/c/:calendarId/e/:eventId` — workspace + карточка события
- `/search` — поиск календарей/событий - `/search` — поиск календарей/событий
- `/bookings` — grouped inbox: (A) к подтверждению (owner/specialist pending) + - `/bookings` — grouped inbox: (A) к подтверждению (owner/specialist pending) +
(B) мои записи участника; deep-link в `/c/.../e/...` (B) мои записи участника; deep-link в `/c/.../e/...`
- `/calendars` — управление своими календарями (CRUD) - `/calendars` — управление своими календарями (create только Студия; personal delete UI скрыт)
- `/more` — вторичное меню + language switcher (**без** mood); пункт specialist - `/more` — вторичное меню + language switcher (**без** mood); пункт specialist
invites **скрыт**, если входящих pending = 0; при pending > 0 — badge со счётчиком invites **скрыт**, если входящих pending = 0; при pending > 0 — badge со счётчиком
(deep-link `/invites?token=` работает) (deep-link `/invites?token=` работает)
@@ -193,24 +206,39 @@ cover (`.eh-discover-media--photo`); иначе mood wash + initials (`.eh-disco
- Создание / апгрейд `commercial` без **уже active** sub/trial → `402``/subscription` - Создание / апгрейд `commercial` без **уже active** sub/trial → `402``/subscription`
(trial — явный `start_trial`, не auto при create); после оплаты — (trial — явный `start_trial`, не auto при create); после оплаты —
возврат к созданию/редактированию возврат к созданию/редактированию
- Владелец commercial: специалисты **через invite** (не сырой `user_id`): - Владелец commercial: команда — owner-строка specialist (создаётся бэком при create
commercial) + остальные **через invite** (не сырой `user_id`):
typeahead `GET /v1/users/lookup` и/или email → `POST …/specialist-invites`; typeahead `GET /v1/users/lookup` и/или email → `POST …/specialist-invites`;
список active + исходящие pending; deactivate/remove принятых. список + исходящие pending; deactivate/remove принятых (**не** owner).
Выбор `specialist_id` в форме события (только `active`). Выбор `specialist_id` в форме события (только `active`).
Баннер «подписка истекла / истекает» при `booking_open=false`. Баннер «подписка истекла / истекает» при `booking_open=false`.
`SpecialistsPanel` (owner): mobile density — имя/теги сверху, actions снизу `SpecialistsPanel` (owner): во вкладке **Команда** rail (desktop) / segmented
(mobile) — **не** внутри About; **owner всегда первым**, бейдж «Владелец»,
галочка «Я специалист» ↔ `PUT status` active|inactive; edit name/specialization;
Delete для owner скрыт. Mobile density — имя/теги сверху, actions снизу
full-width (invite/deactivate не ломать); desktop — вертикальный rail + scroll. full-width (invite/deactivate не ломать); desktop — вертикальный rail + scroll.
Режимы сетки **Студия** (агрегат `start_time`+`duration`, заливка booked/total,
label `HH:mm · K/N свободно`, popover мастеров) и **Мастер** (фильтр
`specialist_id` + sticky roster как D9 B) — см. §3.1. Месяц в Студии —
density markers. Occupancy ячеек/popover — из API `booking_occupancy`
(`"free"` | `"pending"` | `"confirmed"`, BackSpec events); ★ `0.0` на free
не показывать. Personal — без агрегата / без вкладки Команда.
- Guest browse commercial: roster активных → фильтр расписания по `specialist_id` - Guest browse commercial: roster активных → фильтр расписания по `specialist_id`
(D9 B; см. §3.1); owner panel не смешивать с guest roster. (D9 B; см. §3.1); в «Все» — studio-агрегат как у owner; owner panel (invite/CRUD)
не смешивать с guest roster.
- Invitee: inbox входящих `GET /v1/user/specialist-invites` на `/invites` - Invitee: inbox входящих `GET /v1/user/specialist-invites` на `/invites`
принять / отклонить; deep-link из email → accept по `token` после логина. принять / отклонить; deep-link из email → accept по `token` после логина.
В More пункт «Приглашения» показывается **только** при `pending > 0` с badge В More пункт «Приглашения» показывается **только** при `pending > 0` с badge
счётчика (не путать с booking-заявками на `/bookings`). счётчика (не путать с booking-заявками на `/bookings`).
### 5.5. События ### 5.5. События
- Список: `GET /v1/calendars/:calendar_id/events` - Список: `GET /v1/calendars/:calendar_id/events` — для commercial в JSON события
опционально `booking_occupancy` (`"free"` | `"pending"` | `"confirmed"`;
приоритет confirmed > pending > free; см. BackSpec §6). Studio-агрегат /
popover мастеров опираются на это поле (+ capacity/bookings при необходимости).
- CRUD владельца: `POST/PUT/DELETE` (в т.ч. опциональный `specialist_id`) - CRUD владельца: `POST/PUT/DELETE` (в т.ч. опциональный `specialist_id`)
- Детали: `GET /v1/events/:id` (отображение специалиста, если задан) - Детали: `GET /v1/events/:id` (отображение специалиста, если задан;
тот же `booking_occupancy`)
- Вхождения: `GET /v1/events/:id/occurrences` - Вхождения: `GET /v1/events/:id/occurrences`
- Отмена вхождения: `DELETE /v1/events/:id/occurrences/:start_time` - Отмена вхождения: `DELETE /v1/events/:id/occurrences/:start_time`