Маркетинговая информация — Список

Роуты:

  • /marketing — выбор ТТ (если у пользователя > 1 ТТ в scope)
  • /marketing/stores/{storeId} — список слайдов выбранной ТТ

API: GET /api/v1/admin/stores/{storeId}/marketing-slides

Статус реализации

  • Backend: не начат — эндпоинты marketing-slides/* ещё не реализованы в erp-store-service (см. декомпозиция Store Service)
  • Фронт: не начат — в erp-admin/web/src/pages/ нет папки marketing/
  • Sidebar-пункт: ещё не добавлен в components/layout/Layout.tsx
  • Permission-guard: marketing.read / marketing.write ещё не в каталоге User Service (миграция требуется)

Источники


Новый пункт в TOP-секции левого сайдбара (Layout.tsx):

А Альфа ERP
├─ Dashboard
├─ Юридические лица
├─ Торговые точки
├─ Маркетинг          ← новый
├─ ...
  • Permission-guard: marketing.read (если permission нет — пункт скрыт через UI-гейтинг)
  • Иконка — постер / picture
  • Клик → /marketing

Экран 1: Выбор ТТ (/marketing)

Если у пользователя в scope больше одной ТТ — показываем выбор. Если scope = одна ТТ (типичный кейс владельца партнёра с одной ТТ или менеджера) — сразу redirect на /marketing/stores/{его_единственная_ТТ}.

Layout

Заголовок «Маркетинговая информация», под ним — таблица ТТ доступных пользователю.

КолонкаДанные
Название ТТКликабельно — переход на /marketing/stores/{id}
Адрес
Город
Активных слайдовЦелое число (агрегат из Store Service — отдельный запрос или денормализованное поле в списке ТТ)
Конфиг standby«5 мин · 9 сек» — standby_idle_minutes + standby_transition_seconds. Клик → форма редактирования ТТ (вкладка «Настройки»)

Источник данных

GET /api/v1/stores?per_page=100 — берём список ТТ scope’а пользователя.

Для каждого ряда — счётчик активных слайдов:

  • Вариант 1 (предпочтительно): в /admin/stores/{id}/marketing-slides?active=true берём meta.total или data.length параллельно (N запросов = N ТТ; для MVP с ≤ 10 ТТ норм)
  • Вариант 2: добавить агрегат в GET /api/v1/storesmarketing_active_count поле (требует расширения Store Service)

В MVP — Вариант 1.

Фильтры / Поиск

  • Поиск по названию ТТ
  • Фильтр по городу

Состояния

СостояниеЧто показываем
ЗагрузкаSkeleton-таблица
Пусто«У вас нет торговых точек» (для пользователей с пустым scope)
Одна ТТАвто-редирект на /marketing/stores/{id}

Экран 2: Список слайдов ТТ (/marketing/stores/{storeId})

Layout

[← Все ТТ]   Маркетинговая информация → ТТ «Арбат-флагман»

[Превью карусели]  [+ Загрузить слайд]  [✨ Сгенерировать через AI]

┌─────────────────────────────────────────────────────────────┐
│ ⋮⋮ │ [thumb] │ Заголовок         │ Источник │ Активен │ ... │
├─────────────────────────────────────────────────────────────┤
│ ⋮⋮ │  16:9   │ Hero CTA          │ Загружен │  ☑      │ ⋮   │
│ ⋮⋮ │  16:9   │ Замена стека      │ Загружен │  ☑      │ ⋮   │
│ ⋮⋮ │  16:9   │ AI inside         │ AI Photo │  ☑      │ ⋮   │
└─────────────────────────────────────────────────────────────┘

(пагинация если > 20)

Колонки таблицы

КолонкаДанныеПримечание
Drag-handleИконка ⋮⋮ для grab-and-drop
Превьюimage_urlMiniature 60×60 с object-fit:cover, ratio 16:9 letterbox
ЗаголовокtitleТёмный текст
ИсточникsourceChip: Загружено (нейтральный) / AI Photo Studio (с лого AI). Клик в AI-источник → tooltip с source_ref.preset_id / source_ref.job_id
АктивенactiveToggle (switch). Изменение → PUT с { active: true/false }
Созданcreated_atDD.MM.YYYY HH:mm
ДействияМеню (см. ниже)

Сортировка

  • По умолчанию — order ASC (как на кассе)
  • Drag-and-drop изменяет порядок — клиент перестраивает массив локально, при отпускании отправляет PATCH reorder с массивом [{id, order}]

Действия

Заголовок страницы:

КнопкаActionВидимость
← Все ТТBack to /marketingЕсли scope > 1 ТТ
Превью каруселиОткрывает модалку с auto-transition (как на POS) для проверкиВсегда
+ Загрузить слайдОткрывает форму создания (см. ниже)marketing.write
✨ Сгенерировать через AIUI-заглушка с tooltip «Будет в BR 6.2» (disabled)marketing.write + gensvc.photo.create

Меню действий строки (трёхточечное):

ДействиеВидимостьЧто происходит
Редактироватьmarketing.writeПереход на [[09-Frontend Specs/Админка Франшизы/Маркетинговая информация — Редактирование слайда|Редактирование]]
Скачать оригиналmarketing.readwindow.open(image_url)
Удалитьmarketing.writeМодалка подтверждения → DELETE → обновить список

Drag-and-drop

Реализуется по образцу TablesSection.tsx (ручной handler без библиотек):

  1. onMouseDown на drag-handle — фиксируем dragIndex
  2. onMouseMove — отслеживаем над какой строкой курсор, перерисовываем placeholder
  3. onMouseUp — берём новый порядок, отправляем PATCH reorder:
    PATCH /api/v1/admin/stores/{storeId}/marketing-slides/reorder
    { "order": [{ "id": "uuid-1", "order": 0 }, { "id": "uuid-2", "order": 1 }, ...] }
    
  4. Optimistic UI — порядок применяется до ответа. При ошибке — откат

Загрузка нового слайда

Кнопка «+ Загрузить слайд» открывает модалку.

Поля формы

ПолеТипRequiredВалидация
КартинкаFile input (drag-and-drop zone)ДаТолько image/jpeg, image/png, image/webp; ≤ 10 МБ; разрешение ≥ 1280×720
ЗаголовокInputДа≤ 200 символов
АктивенSwitchНетDefault true

Submit

POST /api/v1/admin/stores/{storeId}/marketing-slides через admin-bff как multipart:

POST /admin/api/v1/stores/{storeId}/marketing-slides
Content-Type: multipart/form-data

file: <binary>
title: "Hero CTA"
active: true

После успеха — закрыть модалку, добавить слайд в список (или reload).

Состояния формы

СостояниеЧто показываем
DefaultПоля + drag-and-drop зона с подсказкой «Перетащите картинку или нажмите»
Файл выбранПревью + размер + разрешение, валидация на лету
SubmittingDisabled-форма, лоадер на кнопке
ОшибкаToast с текстом ошибки (VALIDATION_ERROR, MARKETING_SLIDES_LIMIT_REACHED, S3_UPLOAD_FAILED)

Лимит активных слайдов

При попытке создать 21-й активный слайд бэк вернёт MARKETING_SLIDES_LIMIT_REACHED. UI показывает:

Достигнут лимит активных слайдов (20)

Деактивируйте один из существующих, чтобы добавить новый.


Превью карусели (модалка)

При клике «Превью карусели»:

  • Открывается модалка с aspect-ratio 16:9 (full-screen на мобиле, центральный блок на десктопе)
  • В неё подгружаются те же активные слайды, что отдаст бэк по GET .../marketing-slides?active=true
  • Auto-transition через standby_transition_seconds (берём из карточки ТТ)
  • Кнопки: «Закрыть», «← Предыдущий», «Следующий →»
  • Эта модалка имитирует то, что увидит кассир. Не использует SSE — это локальный preview.

Empty state

Если у ТТ нет ни одного слайда:

Здесь пока ничего нет

Загрузите первый слайд для standby-карусели,
чтобы на кассе крутился ваш бренд

[+ Загрузить слайд]   [✨ Сгенерировать через AI]

Если есть слайды, но все неактивные:

☑ Покажите хотя бы один слайд

У вас 5 слайдов, но все деактивированы.
Касса в standby показывает только активные.

Конфигурация standby (standby_idle_minutes / standby_transition_seconds)

Поля не на этом экране. Они управляются в карточке ТТ (см. Торговые точки — Карточка) или вкладке «Настройки» в той же карточке. На странице слайдов — только информационная плашка:

ⓘ Standby запускается через 5 мин, между слайдами 9 сек.
   Изменить →  /stores/{id}/edit#standby

Ролевой доступ

По Ролевой матрице:

РольПоведение
Владелец франшизыВидит все ТТ франшизы, может всё
Владелец партнёраВидит свои ТТ, может всё в их пределах
Обычный с marketing.readВидит свои ТТ (из роли), список read-only
Обычный с marketing.writeВидит свои ТТ, может всё
Без permissionПункт меню скрыт; прямой URL → 403 NoAccessPage

Переходы

ОткудаКудаТриггер
Sidebar/marketingКлик «Маркетинг»
/marketing/marketing/stores/{id}Клик по строке ТТ
/marketing/stores/{id}Редактирование слайдаМеню → «Редактировать»
/marketing/stores/{id}Карточка ТТКлик по плашке конфига standby

Ссылки