From 546f888487caa92f0e23ff532ea283105a93fe71 Mon Sep 17 00:00:00 2001 From: Aleksey Sabilin Date: Mon, 27 Jul 2026 17:53:23 +0300 Subject: [PATCH] Docs: sync booking expired (Back#60) + Front#31 UX polish. Fixes EventHub/EventHubSpec#13 --- EventHubBackSpec.md | 42 ++++++++++++++++++++++++++------------- EventHubFrontSpec.md | 35 ++++++++++++++++++++++---------- WORKFLOW.md | 1 + design/front/UI-PARITY.md | 25 ++++++++++++++++++----- 4 files changed, 74 insertions(+), 29 deletions(-) diff --git a/EventHubBackSpec.md b/EventHubBackSpec.md index 1b3d97a..5571458 100644 --- a/EventHubBackSpec.md +++ b/EventHubBackSpec.md @@ -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`). diff --git a/EventHubFrontSpec.md b/EventHubFrontSpec.md index 3e0b5e9..f0a662c 100644 --- a/EventHubFrontSpec.md +++ b/EventHubFrontSpec.md @@ -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. Отзывы diff --git a/WORKFLOW.md b/WORKFLOW.md index 666aac3..de87dd3 100644 --- a/WORKFLOW.md +++ b/WORKFLOW.md @@ -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 | diff --git a/design/front/UI-PARITY.md b/design/front/UI-PARITY.md index c935cbc..e258d0e 100644 --- a/design/front/UI-PARITY.md +++ b/design/front/UI-PARITY.md @@ -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).