diff --git a/EventHubFrontSpec.md b/EventHubFrontSpec.md index b116f27..9194e5c 100644 --- a/EventHubFrontSpec.md +++ b/EventHubFrontSpec.md @@ -59,39 +59,50 @@ Deep-link на календарь, владельцем которого явл `pending=0`; при `pending > 0` — badge со счётчиком (deep-link `/invites` работает). **Mood в Ещё нет** — chip в AppShell рядом с BrandWordmark (Front#40; ранее Front#39 в chrome workspace). -- **Mood + Lens (Front#39/#40 + controls IA):** mood chip (FLOW/MOMENTUM/CONTROL → sheet picker) — - в **AppShell** рядом с логотипом (**не** в toolbar Month/Week/Day). Workspace chrome - разнесён по зонам: (1) селектор календаря; (2) период + вид Месяц/Неделя/День; - (3) owner schedule actions («+ Новое событие», «Заполнить расписание» — отдельный ряд); - (4) lens «Обзор / На сегодня / Заявки» — **в шапке** collapsible lens strip (underline/text - tabs на desktop, sheet на mobile; **не** рядом с ViewMode chips). Strip — Front-only derive - из уже загруженных events/bookings. Без flip календаря; без 4-го CREATOR. Strip по умолчанию - раскрыт на desktop, свёрнут на mobile. Нет на HTML-архиве месяца. +- **Mood + Lens (Front#39/#40 + Front#46):** mood chip (FLOW/MOMENTUM/CONTROL → sheet picker) — + в **AppShell** рядом с логотипом (**не** в toolbar Month/Week/Day). **Lens** — инфо-табло + **под строкой AppShell** (logo/mood/nav) на calendar workspace: сегменты Обзор / Сегодня / + Заявки + метрики/chips (не рядом с ViewMode). Soft-default по mood **без auto-pin** при + клике на вкладку; при смене mood — `DEFAULT_LENS_BY_MOOD`. Workspace chrome: (1) селектор; + (2) период + вид Месяц/Неделя/День; (3) owner actions — для commercial только + «+ Новое событие» («Заполнить расписание» — во вкладке Студия). Без flip / CREATOR. + Нет lens на HTML-архиве месяца / не-calendar routes. - **Week view (Front#40):** одна строка day-headers (без дубля заголовков `WeekDayColumn`); горизонтальный скролл через `.eh-cal-week-scroll` (mobile ~390). -- Контекст виджета: свой календарь (селектор) или чужой (browse после поиска). Чужой `personal` — +- Контекст виджета: свой календарь (селектор) или browse чужого (после поиска). Чужой `personal` — только просмотр; чужой `commercial` с `booking_open=true` — запись; при `booking_open=false` - (restricted / нет подписки владельца) — просмотр + сообщение «Запись временно недоступна». -- **Свой default personal:** поверх сетки — overlay confirmed записей на чужие commercial - слоты (Front-only: `GET /v1/user/bookings` → events/calendars). Заголовок - `событие · студия · специалист`; клик → `/c/:foreignCalId/e/:eventId` (не в своём cal). - Дополнительно (Front#43): overlay **своих** рабочих слотов commercial, где - `specialist_id` = текущий user — gate через accepted `GET /v1/user/specialist-invites` - + проверка active в `GET …/specialists`, затем `GET …/events?from&to`; title - `событие · студия`. Personal владельца без accepted invite / без своих слотов - чужой roster **не** зеркалит. + — просмотр + «Запись временно недоступна». Бейдж «чужой» **не** показывать; CTA + **«Мой календарь»** + `ChevronLeft` → `/default`. +- **Единственный personal** (Back: title `Default`, UI «По умолчанию»): overlay confirmed + записей на чужие commercial + specialist duty (Front#35/#43) — **только** на нём, не на + студиях. Create calendar UI — только тип **Студия** (`commercial`). Селектор: «По умолчанию» + + студии. - **Guest на чужом commercial (D9 модель B):** roster карточек активных специалистов → тап → сетка только со слотами `event.specialist_id` = выбранный; «Все слоты студии» снимает фильтр. Это **не** chip-filter на общей сетке. Book как обычно — без `specialist` в body заявки (слот уже привязан к спецу на событии). -- Agenda rail: empty copy зависит от типа календаря — personal «Нет событий», commercial - «Нет слотов». -- Выбор события открывает карточку действий: mobile — bottom sheet; desktop — боковая панель. Действия зависят от роли (owner / participant). -- Без выбранного события — панель «О календаре» (описание, title/meta, рейтинг, отзывы). - Форма отзыва на календарь/событие — только при confirmed booking; жалоба доступна без записи. - На чужом календаре — CTA **«Отслеживать» / Follow** (не путать с платной «Подписка»); - follow не открывает форму отзыва. -- На чужом `/c/:id` — CTA «Мой календарь» и таб Calendar не `aria-current` (intent `browse`). + В режиме «Все» — тот же **studio-агрегат** слотов, что у owner «Студия» (см. ниже); + фильтр D9 **без** второго UI режима. +- **Owner commercial workspace — режимы Студия / Мастер:** + - **Студия** — агрегат слотов по `start_time` + `duration`: одна ячейка с заливкой + `booked/total`, label `HH:mm · K/N свободно`; клик → popover мастеров со статусами + (занятость по `booking_occupancy` / active bookings, BackSpec §6 events). + - **Мастер** — фильтр сетки по `specialist_id`; sticky roster как guest D9 B + (**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` (из «Ещё»). - Открытие события — `navigate` push (не `replace`), чтобы Back возвращал к календарю. - Owner: WS subscribe на активный календарь; `PUT` события из карточки; `PUT` календаря на `/calendars`. @@ -127,12 +138,10 @@ Deep-link на календарь, владельцем которого явл Time Arc appicon — только favicon, не в switcher. **Lens** (отдельно от mood и от view month/week/day): `overview` / `today` / `bookings` -(UI: Обзор / На сегодня / Заявки). Переключатель — в шапке lens strip (не в toolbar рядом -с Месяц/Неделя/День). Persist только `localStorage` (`eh.calendar.lens` + -`eh.calendar.lens.pinned`); **не** поле профиля / Back API. Soft-default при смене mood, -если lens не pinned пользователем: FLOW→overview, MOMENTUM→today, CONTROL→bookings. -Strip над сеткой: free gaps сегодня / события сегодня / upcoming bookings (derive -на клиенте из уже загруженных данных). +(UI: Обзор / Сегодня / Заявки). Инфо-табло **под AppShell** на calendar routes (не в +toolbar ViewMode). Persist: session preference + soft LS; клик по вкладке **не** ставит +pin навсегда; при смене mood — soft-default FLOW→overview, MOMENTUM→today, +CONTROL→bookings. Контент: free gaps / события сегодня / upcoming bookings (Front-derive). Язык 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 визита; тап открывает студию. +**v3 (дуга между календарями):** `/following` сортирует студии по ближайшему Time Arc; тап открывает неделю студии с курсором на этом дне (`data-time-arc` как в v1). Discover остаётся списком: на строке calendar — «когда» (`data-time-arc-when`), без общей сетки нескольких студий. Ghosts владельца считаются по активной студии (переключение календаря не смешивает паттерны). По-прежнему без LLM / Back AI. + ## 4. Маршруты Публичные: - `/login`, `/register`, `/verify` Защищённые (`ProtectedRoute`): -- `/` — Calendar Workspace (автовыбор своего календаря или empty-state) +- `/default` — resolve единственного personal → `/c/:id` (post-login и tab «Календарь») +- `/default/e/:eventId` — то же + карточка события +- `/` — alias → `/default` - `/c/:calendarId` — workspace с календарём - `/c/:calendarId/e/:eventId` — workspace + карточка события - `/search` — поиск календарей/событий - `/bookings` — grouped inbox: (A) к подтверждению (owner/specialist pending) + (B) мои записи участника; deep-link в `/c/.../e/...` -- `/calendars` — управление своими календарями (CRUD) +- `/calendars` — управление своими календарями (create только Студия; personal delete UI скрыт) - `/more` — вторичное меню + language switcher (**без** mood); пункт specialist invites **скрыт**, если входящих pending = 0; при pending > 0 — badge со счётчиком (deep-link `/invites?token=` работает) @@ -193,24 +206,39 @@ cover (`.eh-discover-media--photo`); иначе mood wash + initials (`.eh-disco - Создание / апгрейд `commercial` без **уже active** sub/trial → `402` → `/subscription` (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`; - список active + исходящие pending; deactivate/remove принятых. + список + исходящие pending; deactivate/remove принятых (**не** owner). Выбор `specialist_id` в форме события (только `active`). Баннер «подписка истекла / истекает» при `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. + Режимы сетки **Студия** (агрегат `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` - (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` — принять / отклонить; deep-link из email → accept по `token` после логина. В More пункт «Приглашения» показывается **только** при `pending > 0` с badge счётчика (не путать с booking-заявками на `/bookings`). ### 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`) -- Детали: `GET /v1/events/:id` (отображение специалиста, если задан) +- Детали: `GET /v1/events/:id` (отображение специалиста, если задан; + тот же `booking_occupancy`) - Вхождения: `GET /v1/events/:id/occurrences` - Отмена вхождения: `DELETE /v1/events/:id/occurrences/:start_time`