Docs: sync booking expired (Back#60) + Front#31 UX polish. Fixes EventHub/EventHubSpec#13

This commit is contained in:
2026-07-27 17:53:23 +03:00
parent 7c21fa2aef
commit 546f888487
4 changed files with 74 additions and 29 deletions
+28 -14
View File
@@ -95,13 +95,19 @@ pending владельца, **не** меняет `calendar.type`. Legacy `downg
- `POST /v1/events/:id/bookings` только если календарь события commercial, `booking_open=true`,
событие `active`, есть свободная вместимость.
- **Pending занимает capacity** наравне с confirmed (защита от overbook при auto/timeout).
- Capacity: число booking со статусом `pending` | `confirmed`; `cancelled` не считаются.
- Capacity: число booking со статусом `pending` | `confirmed`; `cancelled` и `expired` не считаются.
- Политика `confirmation` календаря при создании booking:
- `auto` → сразу `confirmed` + `confirmed_at`;
- `manual``pending`; confirm/decline — владелец или specialist (см. ниже);
- `{timeout, N}``pending`; через N секунд без решения: auto-confirm, если ещё есть
capacity, иначе `cancelled` (`timeout_full`).
- Участник: `DELETE /v1/bookings/:id` — отмена своей pending/confirmed.
- **Past-pending → `expired`:** если событие уже началось (`now >= event.start_time`), а booking
ещё `pending`, статус переводится в `expired` (lazy при чтении списков/`GET` booking и в
`process_timeout_bookings`). `expired` не actionable: Confirm/Decline → `409` (`Booking expired`).
- **Inbox:** `GET /v1/user/booking-requests` возвращает только ещё actionable pending
(до старта события); past-pending помечает `expired` и **не** включает в ответ.
- Участник: `DELETE /v1/bookings/:id` — отмена своей pending/confirmed; для уже `expired`
no-op успех (как для `cancelled`).
- WS: `booking_update` участнику и владельцу (и specialist при confirm на «своём» событии).
#### Специалисты
@@ -171,8 +177,9 @@ Legacy: прямой `POST /v1/calendars/:id/specialists` **не использ
(иначе `400`).
- **Confirm/decline booking:** владелец — любые booking календаря; `active` specialist — только
booking на событиях, где `event.specialist_id` = его `user_id`.
- **Inbox к подтверждению:** `GET /v1/user/booking-requests` агрегирует pending по тем же
правилам (owner — все события своих календарей; specialist — только свои слоты).
- **Inbox к подтверждению:** `GET /v1/user/booking-requests` агрегирует **actionable** pending
по тем же правилам (owner — все события своих календарей; specialist — только свои слоты);
past-pending → `expired` и из ответа исключается.
#### Фаза 2 (вне текущего контракта реализации)
- `calendar_share` (`read` | `write` | `admin`): не путать с follow, specialist_invite и платной subscription.
@@ -247,12 +254,17 @@ Legacy: прямой `POST /v1/calendars/:id/specialists` **не использ
- Запись доступна только на событиях **commercial**-календаря с `booking_open=true` (§2.1.2).
- На personal → `403` (`personal_calendar`); при restricted commercial → `403` (`subscription_inactive`).
- В зависимости от `confirmation` календаря: auto / manual / timeout (§2.1.2).
- Статусы: `pending`, `confirmed`, `cancelled`.
- **Capacity:** лимит слотов считают `pending` + `confirmed` (pending резервирует место).
- Участник может отменить свою запись (`DELETE`).
- Статусы: `pending`, `confirmed`, `cancelled`, `expired`.
- **Capacity:** лимит слотов считают `pending` + `confirmed` (pending резервирует место;
`cancelled` / `expired` не занимают).
- **Истечение:** past-pending (событие уже началось) → `expired` (lazy + timeout job); не
путать с `specialist_invite.status=expired`.
- Участник может отменить свою запись (`DELETE`) для pending/confirmed; `expired`/`cancelled`
идемпотентный успех.
- Confirm/decline: владелец календаря — любые заявки; active specialist — только на событиях
со своим `specialist_id` (§2.1.2).
со своим `specialist_id` (§2.1.2). На `expired` (и после lazy-mark) → `409`.
- При подтверждении фиксируется `confirmed_at`.
- Inbox `GET /v1/user/booking-requests` — только pending до старта события (§2.1.2).
### 2.4. Отзывы и рейтинги
- Пользователи могут оставлять отзывы (рейтинг 1–5 и комментарий) на события или календари.
@@ -456,11 +468,11 @@ src/
пара `current_password` + `password` (неверный текущий → `403`). Нельзя менять
email/role/status; неизвестные поля → `400`. Ответ — полный профиль как GET.
- `GET /v1/user/bookings` — бронирования пользователя **как участника**.
- `GET /v1/user/booking-requests` — pending-заявки к подтверждению, где текущий
пользователь — **owner** календаря события или **assigned specialist**
(`event.specialist_id` = user и specialist active). Ответ — массив объектов
booking + `role` (`owner`|`specialist`) + вложенный `event`
(`id`, `calendar_id`, `calendar_title`, `title`, `start_time`, `duration`,
- `GET /v1/user/booking-requests` **actionable** pending-заявки к подтверждению, где
текущий пользователь — **owner** календаря события или **assigned specialist**
(`event.specialist_id` = user и specialist active). Past-pending помечается `expired` и
**не** попадает в ответ. Ответ — массив объектов booking + `role` (`owner`|`specialist`) +
вложенный `event` (`id`, `calendar_id`, `calendar_title`, `title`, `start_time`, `duration`,
`specialist_id`). Confirm/decline — существующий `PUT /v1/bookings/:id`.
- `GET /v1/user/reviews` — отзывы пользователя.
- `GET /v1/user/following` — календари, которые пользователь отслеживает (follow).
@@ -494,7 +506,9 @@ src/
- `GET /v1/bookings/:id` — статус бронирования.
- `PUT /v1/bookings/:id` — подтвердить/отклонить (`confirm`|`decline`); владелец —
любые booking календаря; active specialist — только события со своим `specialist_id`.
- `DELETE /v1/bookings/:id` — отменить бронирование (участник).
На past-pending / уже `expired``409` (`Booking expired`); при полной вместимости на
confirm → `409` (`Event is full`).
- `DELETE /v1/bookings/:id` — отменить бронирование (участник; `expired`/`cancelled` — no-op `200`).
- `POST /v1/reviews` — создать отзыв.
- `GET /v1/reviews` — список отзывов (поле `my_vote` для текущего пользователя).
- `GET /v1/reviews/:id` — отзыв по ID (`my_vote`).
+25 -10
View File
@@ -53,10 +53,15 @@ Deep-link на календарь, владельцем которого явл
- Главные вкладки: **Календарь** (`/`, `/c/:id`), **Найти** (`/search`), **Записи** (`/bookings`), **Ещё** (`/more`).
- **Записи** (`/bookings`) — grouped inbox: «К подтверждению» (owner/specialist pending + Confirm/Decline)
и «Мои записи» (participant); пустые группы скрывать. Confirm на карточке события остаётся.
- В **Ещё**: пункт входящих specialist invites скрыт при `pending=0` (deep-link `/invites` работает).
Past/expired: бейдж `expired` («истекла»), Cancel скрыт; empty-state с CTA Discover / календари.
Время на list-карточках — вторичное к title (`.eh-book-list-time`).
- В **Ещё**: пункт входящих specialist invites скрыт при `pending=0`; при `pending > 0`
badge со счётчиком (deep-link `/invites` работает).
- Контекст виджета: свой календарь (селектор) или чужой (browse после поиска). Чужой `personal`
только просмотр; чужой `commercial` с `booking_open=true` — запись; при `booking_open=false`
(restricted / нет подписки владельца) — просмотр + сообщение «Запись временно недоступна».
- Agenda rail: empty copy зависит от типа календаря — personal «Нет событий», commercial
«Нет слотов».
- Выбор события открывает карточку действий: mobile — bottom sheet; desktop — боковая панель. Действия зависят от роли (owner / participant).
- Без выбранного события — панель «О календаре» (описание, title/meta, рейтинг, отзывы).
Форма отзыва на календарь/событие — только при confirmed booking; жалоба доступна без записи.
@@ -69,7 +74,9 @@ Deep-link на календарь, владельцем которого явл
- Поиск (`/search`): query/filters в URL params (восстановление при возврате); chip type фильтрует
discovery tops без ухода из Popular; в строке результата — id snippet и `calendar_title` для event.
- CRUD своих календарей — `/calendars` (из «Ещё»). Legacy `/discover`, `/calendars/:id` → redirects.
- На мобиле — bottom tab bar (+ safe-area); на desktop — sticky header + табы.
- На мобиле — bottom tab bar (+ safe-area); на desktop — **одна строка** chrome:
logo | nav tabs | nickname + logout (не два ряда header+nav). Сырой email в chrome
не показывать — primary identity = `nickname` (fallback без `@`).
Nav `aria-current` синхронизирован с `useLocation` (без remount `Outlet` по pathname).
- Mood themes — см. §3.2.
@@ -85,7 +92,7 @@ Deep-link на календарь, владельцем которого явл
| MOMENTUM | `energetic` | тёмный ink + coral `#FF6A3D` |
| CONTROL | `business` | charcoal + steel `#324A5F`, более жёсткие радиусы |
Переключатели mood и языка: на экранах auth (`/login`, `/register`, `/verify`) и в `/more`. В шапке workspace — wordmark CalenTIQ (Time Arc), email и выход (без mood/lang).
Переключатели mood и языка: на экранах auth (`/login`, `/register`, `/verify`) и в `/more`. В шапке workspace — wordmark CalenTIQ (Time Arc), **nickname** (не raw email) и выход (без mood/lang). Desktop chrome — одна строка: logo | nav | logout.
Язык UI (`ru`/`en`): до логина — `navigator.language`; после входа — поле `language` профиля (`PATCH /v1/user/me`). Если в профиле язык пуст — при логине записывается текущий (браузерный/выбранный на экране входа). Язык также можно сменить в форме `/profile`.
@@ -103,7 +110,8 @@ Deep-link на календарь, владельцем которого явл
(B) мои записи участника; deep-link в `/c/.../e/...`
- `/calendars` — управление своими календарями (CRUD)
- `/more` — вторичное меню + mood switcher; пункт specialist invites **скрыт**,
если входящих pending = 0 (deep-link `/invites?token=` работает)
если входящих pending = 0; при pending > 0 — badge со счётчиком
(deep-link `/invites?token=` работает)
- `/invites` — inbox входящих specialist invites (не смешивать с `/bookings`)
- `/following` — отслеживаемые чужие календари (follow; не платная Подписка)
- `/reviews`, `/subscription`, `/tickets`, `/profile`
@@ -122,7 +130,9 @@ Redirects: `/discover` → `/search`; `/calendars/:id` → `/c/:id`; `/calendars
### 5.3. Поиск (участник)
`GET /v1/search` — фильтры (`type`, `q`, `tags`, даты, `lat`/`lon`/`radius`, sort, order, пагинация). Без `q`/дат/tags/geo/sort — топы календарей и событий; иначе — поиск. В списке бейдж типа — `calendar`|`event` (не `personal`/`commercial`). Даты, теги, geo и сортировка свёрнуты по умолчанию. Переход к `/c/:id` или `/c/:id/e/:eventId`.
В результатах calendar (и event, если Back отдаёт) может быть `image_url` — Discover показывает cover; иначе mood wash + initials. **Upload API нет** — поле URL (seed / ручная установка).
В результатах calendar (и event, если Back отдаёт) может быть `image_url` — Discover показывает
cover (`.eh-discover-media--photo`); иначе mood wash + initials (`.eh-discover-media--fallback`,
не fake photo-hero). Битый `image_url` → fallback. **Upload API нет** — поле URL (seed / ручная установка).
### 5.4. Календари
- Список своих: `GET /v1/calendars`
@@ -141,8 +151,8 @@ Redirects: `/discover` → `/search`; `/calendars/:id` → `/c/:id`; `/calendars
Баннер «подписка истекла / истекает» при `booking_open=false`.
- Invitee: inbox входящих `GET /v1/user/specialist-invites` на `/invites`
принять / отклонить; deep-link из email → accept по `token` после логина.
В More пункт «Приглашения» показывается **только** при `pending > 0`
(не путать с booking-заявками на `/bookings`).
В More пункт «Приглашения» показывается **только** при `pending > 0` с badge
счётчика (не путать с booking-заявками на `/bookings`).
### 5.5. События
- Список: `GET /v1/calendars/:calendar_id/events`
@@ -157,11 +167,16 @@ Redirects: `/discover` → `/search`; `/calendars/:id` → `/c/:id`; `/calendars
- Владелец и specialist:
- на карточке события: `GET /v1/events/:id/bookings` + Confirm/Decline (**обязательно**)
- inbox `/bookings` группа «К подтверждению»: `GET /v1/user/booking-requests`
(pending, где user — owner календаря или `event.specialist_id`); те же
`PUT /v1/bookings/:id` `{ action: confirm | decline }`
(actionable pending, где user — owner календаря или `event.specialist_id`; past-pending
Back отдаёт как `expired` и **не** включает в inbox); те же
`PUT /v1/bookings/:id` `{ action: confirm | decline }` (на `expired``409`)
- UI `/bookings`: две группы (пустые скрывать): **К подтверждению** + **Мои записи**
(upcoming/past participant). `specialist_invite` сюда **не** попадает.
- Статусы: `pending` | `confirmed` | `cancelled`; confirmation: `auto` | `manual` | timeout
Empty state: title + hint + CTA Discover / «Мои календари».
Время слота на list-карточках — soft (secondary to title).
- Статусы: `pending` | `confirmed` | `cancelled` | `expired`; confirmation: `auto` | `manual` | timeout.
Past+pending / `expired` в «Мои записи» — бейдж «истекла», Cancel скрыт (в т.ч. на
`EventActionCard`). Defensive: если API ещё отдал past `pending` — UI мапит в `expired`.
- Pending резервирует capacity (как на бэке)
### 5.7. Отзывы
+1
View File
@@ -378,6 +378,7 @@ error rate > 1% **или** mean request > 500 ms (hold ≥2 мин в фазе)
| Дата | Изменение |
|------|-----------|
| 2026-07-27 | Spec sync: booking `expired` (Back#60) + Front#31 UX (desktop chrome, empty bookings, agenda copy, Discover covers, nickname, invites badge) |
| 2026-07-27 | Spec#12: booking-requests + Bookings IA + image_url; правило one-repo-per-agent |
| 2026-07-27 | §6 / STAGE-BROWSER: канон — прямой HTTPS `*.calentiq.com`; proxy `:8787` только DNS-fallback |
| 2026-07-22 | §6: Stage UI в Cursor Browser — `STAGE-BROWSER.md` + alias proxy |
+20 -5
View File
@@ -20,7 +20,11 @@ SoT: [`boards/calentiq-ui-board-final.png`](boards/calentiq-ui-board-final.png).
| Bookings list §D | список заявок с Confirm/Decline на `/bookings` | `/bookings` = **grouped inbox**: (A) К подтверждению — pending owner/specialist + Confirm/Decline; (B) Мои записи — participant upcoming/past + Cancel; empty groups hidden. Event card Confirm/Decline **остаётся** обязательным |
| Login §D phone chrome | три phone frames + (на борде) Google CTA | brand-first hero + mood `--auth-*`; email/password only; mood switcher на login (иконки); taglines mood cards — на `/more`; **без** Google OAuth |
| Favicon set | 3 отдельных favicon файла на борде | mood appicon через `applyMood` |
| Desktop nav | phone bottom tabs на борде | desktop: top tabs (Календарь / Найти / Записи / Ещё); bottom nav — mobile |
| Desktop nav | phone bottom tabs на борде | desktop: **одна строка** chrome — logo \| nav (Календарь / Найти / Записи / Ещё) \| nickname + logout; bottom nav — mobile |
| Header identity | — | nickname primary (не raw email); email только в `title` tooltip |
| Bookings past/expired | — | `expired` бейдж + Cancel скрыт; empty-state с CTA Discover / календари; soft list time |
| Agenda empty copy | — | personal «Нет событий»; commercial «Нет слотов» |
| More invites | пункт меню | скрыт при pending=0; badge счётчика при pending > 0 |
| Stage | полный smoke на stage | **done** 2026-07-27: Login×3 / Calendar×3 + agenda / Discover / CONTROL commercial + Confirm·Decline / More moods |
| Домены calentiq.* | — | публичный stage: `stage.calentiq.com` / `admin.stage.calentiq.com` (канон STAGE-BROWSER.md) |
@@ -32,7 +36,9 @@ IA без sidebar / без Creator mood — совпадает с non-goals (п
**Polish in this pass:** agenda copy/casing/counts + time-first rows; CONTROL steel rail emphasis; neutral Discover cover in mock (не flow-appicon во всех moods).
**Remaining intentional:** no pixel-perfect phone chrome; Discover без photo-hero grid (cover via `image_url` when API returns it); §E specialist schedule card vs panel — не pixel-perfect board mocks (follow-up).
**Front#31 (2026-07-27):** desktop one-row chrome (logo|nav|logout); header nickname; bookings `expired` + soft list time + empty CTA; agenda personal vs commercial empty copy; Discover cover photo/fallback; More invites badge.
**Remaining intentional:** no pixel-perfect phone chrome; Discover без photo-hero grid (cover via `image_url` when API returns it; broken URL → wash+initials); §E specialist schedule card vs panel — не pixel-perfect board mocks (follow-up).
### Bookings inbox + More invites (locked 2026-07-27)
@@ -44,18 +50,25 @@ IA без sidebar / без Creator mood — совпадает с non-goals (п
- **Группа B — Мои записи:** participant bookings (upcoming/past + Cancel) как раньше.
- Пустые группы скрывать (`count=0`).
- Опционально подпись owner vs specialist внутри A; иначе одна секция «К подтверждению».
3. **`specialist_invite` в More:** скрывать пункт меню, если **0 pending** входящих. Deep-link `/invites?token=` работает. Исходящие invite остаются в `SpecialistsPanel`.
3. **`specialist_invite` в More:** скрывать пункт меню, если **0 pending** входящих; при
pending > 0 — **badge** со счётчиком. Deep-link `/invites?token=` работает. Исходящие
invite остаются в `SpecialistsPanel`.
4. **Не** класть строки `specialist_invite` в `/bookings`.
5. **Past-pending / `expired`:** Back#60 помечает past-pending → `expired` и исключает из
`booking-requests`; Confirm/Decline → `409`. UI: бейдж «истекла», Cancel скрыт
(Bookings + EventActionCard); defensive map past+pending → expired badge.
API: `GET /v1/user/booking-requests` (Back) + существующий `PUT /v1/bookings/:id`.
API: `GET /v1/user/booking-requests` (Back) + существующий `PUT /v1/bookings/:id`
(`409` на expired / full).
### Follow-ups (вне этого fix-pass или частично)
| Тема | Статус |
|------|--------|
| Discover `image_url` в search serialization | Back отдаёт поле; upload API **нет** — только URL field / seed |
| Discover `image_url` в search serialization | Back отдаёт поле; upload API **нет** — только URL field / seed; Front#31: photo vs fallback media classes + onError fallback |
| Stage Popular clutter (e2e calendars) | filter notes: см. `EventHubSpec/STAGE-POPULAR-CLEANUP.md`; не трогать реальные smoke данные без фильтра по e2e-паттернам |
| §E specialist schedule card vs panel | deviation: EventActionCard + SpecialistsPanel; отдельный pixel mock — Future |
| Desktop one-row chrome | **done** Front#31 — logo \| nav \| nickname+logout |
## Mood concept = product surface
@@ -180,6 +193,8 @@ CSS-токены и chrome на mood beyond primary hex: surfaces, gradients, ev
| 3 | Discover mood variants | [Front#27](https://git.sabilin.com/EventHub/EventHubFront/issues/27) |
| 4 | CONTROL booking surfaces | [Front#28](https://git.sabilin.com/EventHub/EventHubFront/issues/28) |
| 5 | Polish + stage smoke | [Front#29](https://git.sabilin.com/EventHub/EventHubFront/issues/29) |
| — | P1/P2 UX polish + expired + desktop chrome | [Front#31](https://git.sabilin.com/EventHub/EventHubFront/issues/31) |
| — | Past-pending → `expired` | [Back#60](https://git.sabilin.com/EventHub/EventHubBack/issues/60) |
Эпик: [Spec#10](https://git.sabilin.com/EventHub/EventHubSpec/issues/10).