MVP Spec 11 — Seller Dashboard
Обновлён 1 апр. 2026 г., 12:41 · 0 комментариев
MVP Spec 11 — Seller Dashboard
Паспорт документа
- Статус документа: working spec
- Актуально на: 28 марта 2026 года
- Владелец: backend/platform-команда
- Пересмотр: перед реализацией, при изменении domain rules или при смене source-of-truth по контракту
- Область применения: детальные feature-спеки для product backlog и реализации MVP/v1 направлений
- Связанные документы:
Version: MVP · Priority: P0 · Phase: A (Supply) Status: Draft v1
1. Контекст и цель
Seller Dashboard — главная страница личного кабинета продавца. Это первое, что видит продавец после входа. Дашборд даёт быстрое понимание состояния бизнеса на платформе: сколько курсов опубликовано, сколько людей смотрят, сколько заявок получено.
Цель модуля: дать продавцу мотивирующую, информативную стартовую страницу, которая:
- Показывает ключевые метрики одним взглядом
- Сигнализирует о проблемах (отклонённые/ожидающие айтемы)
- Направляет новых продавцов к созданию первого контента
- Предоставляет быстрый доступ к основным разделам
Ключевые метрики дашборда:
- Количество активных айтемов
- Уникальные просмотры айтемов (total and per period)
- Количество полученных лидов (total and per period)
- Количество полученных отзывов
Что не входит в этот модуль:
- Детальная аналитика по каждому айтему → Spec v1.5 (Analytics)
- Управление айтемами (CRUD) → Spec 02
- Обработка лидов → Spec 09
- Управление сотрудниками → Spec 03
- Управление профилем → Spec 01
- Публичная страница продавца → Spec 12
2. Роли пользователей
| Роль | Действия в этом модуле |
|---|---|
| Seller Owner | Видит все метрики, все айтемы, все уведомления. Полный доступ. |
| Seller Admin CRM | Те же метрики и айтемы что и Owner. |
| Seller Manager | Видит метрики и список айтемов. Не видит финансовые агрегаты (TBD). |
| Seller Teacher | Видит только свои айтемы (Spec 03). Не видит общие метрики. |
3. Use Cases
UC-01: Новый продавец видит welcome-состояние
Актор: Seller (Owner / Admin CRM) — только что зарегистрировался, нет ни одного айтема Предусловие: Seller авторизован, item_count = 0 Триггер: Продавец попадает на /seller (после регистрации или при следующем входе)
Полный поток:
[Точка входа]
→ Продавец завершил онбординг (Spec 01) и получил редирект на /seller
→ ИЛИ: вернулся в кабинет без айтемов
─────────────────────────────────────────────────────────
WELCOME-СОСТОЯНИЕ ДАШБОРДА
─────────────────────────────────────────────────────────
→ Хедер: "Добро пожаловать, {org_name}! 👋"
→ Подзаголовок: "Разместите первый курс — и получите первых клиентов."
→ Пустые метрики (показаны, но с нулями и пояснением):
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Активных курсов │ │ Просмотры │ │ Заявки │ │ Отзывы │
│ 0 │ │ 0 │ │ 0 │ │ 0 │
│ Создайте курс │ │ Появятся когда │ │ Придут когда │ │ Получите первые │
│ первым │ │ есть активный │ │ курс виден │ │ отзывы │
└─────────────────┘ └─────────────────┘ └─────────────────┘ └─────────────────┘
→ Большой CTA-блок в центре страницы:
Иллюстрация (пустая доска или курс-иконка)
Заголовок: "Разместите первый курс"
Текст: "Расскажите о своих занятиях — тысячи родителей ищут именно то, чему вы учите."
Кнопка: "+ Создать первый курс" → /seller/items/new
→ Справа или ниже: 3 шага-инструкция:
① Создайте карточку курса
② Пройдите проверку (обычно 1–2 дня)
③ Начните получать заявки
→ Навигация слева полностью доступна (можно перейти в любой раздел)
UC-01 — Альтернативные потоки
1a. Продавец есть айтемы, но все в статусе draft:
UI-реакция:
→ Welcome-состояние НЕ показывается
→ Показывается стандартный дашборд (UC-02) с нулевыми метриками
→ Баннер-напоминание: "У вас есть черновики. Завершите оформление и отправьте на проверку."
→ Кнопка в баннере: "Перейти к курсам" → /seller/items
1b. Продавец впервые вошёл (тип individual_contributor):
UI-реакция:
→ Тот же welcome-флоу, но текст адаптирован:
Хедер: "Добро пожаловать, {first_name}!"
CTA: "+ Создать первое объявление об услугах"
UC-02: Активный продавец просматривает дашборд
Актор: Seller (Owner / Admin CRM) с хотя бы одним active айтемом Предусловие: Продавец авторизован Триггер: Продавец открывает /seller
Полный поток:
[Точка входа]
→ Продавец вводит /seller в браузере или нажимает "Главная" в навигации кабинета
─────────────────────────────────────────────────────────
ХЕДЕР СТРАНИЦЫ
─────────────────────────────────────────────────────────
→ "Добрый день, {org_name}" (утро/день/вечер — по времени суток)
→ Дата: "25 марта 2026"
→ Кнопка: "+ Создать курс" (quick action, всегда видна)
─────────────────────────────────────────────────────────
ПЕРЕКЛЮЧАТЕЛЬ ПЕРИОДА (Period Selector)
─────────────────────────────────────────────────────────
→ Таб-переключатель: [Сегодня] [Неделя] [Месяц] [Всё время]
Default: Неделя
→ При переключении — метрики пересчитываются без перезагрузки страницы
→ Текущий выбор подсвечивается
─────────────────────────────────────────────────────────
БЛОК КЛЮЧЕВЫХ МЕТРИК (4 карточки)
─────────────────────────────────────────────────────────
Карточка 1: Активных курсов
Число: {active_items_count}
Подпись: "в каталоге прямо сейчас"
(Цифра НЕ зависит от Period Selector — это абсолютное число)
Ссылка: "Управлять" → /seller/items
Карточка 2: Просмотры
Число: {views_count} за выбранный период
Тренд: ↑ +12% vs предыдущий период (если есть данные)
Подпись: "уникальных просмотров курсов"
Ссылка: (нет на MVP, в v1.5 — детальная аналитика)
Карточка 3: Заявки
Число: {leads_count} за выбранный период
Тренд: ↑ +3 vs предыдущий период
Подпись: "новых заявок"
Ссылка: "Все заявки" → /seller/leads
Карточка 4: Отзывы
Число: {reviews_count} total (не зависит от периода)
Средний рейтинг: ★ 4.7
Подпись: "отзывов о ваших курсах"
Ссылка: "Все отзывы" → /seller/reviews
─────────────────────────────────────────────────────────
БЛОК "МОИ КУРСЫ" (топ-список)
─────────────────────────────────────────────────────────
→ Заголовок: "Мои курсы" + кнопка "Все курсы" → /seller/items
→ Список до 5 последних айтемов:
Каждая строка: обложка (thumbnail), название, статус-бейдж, кол-во лидов
→ Если айтемов > 5: кнопка "Показать все ({total})" → /seller/items
─────────────────────────────────────────────────────────
БЛОК "ПОСЛЕДНИЕ ЗАЯВКИ"
─────────────────────────────────────────────────────────
→ Список 3–5 последних необработанных лидов
→ Каждая строка: имя байера, название курса, дата
→ Кнопка: "Все заявки" → /seller/leads
→ Пустое состояние: "Пока заявок нет. Активируйте курсы чтобы начать получать заявки."
─────────────────────────────────────────────────────────
БЛОК БЫСТРЫХ ССЫЛОК (Quick Links)
─────────────────────────────────────────────────────────
→ Карточки-ярлыки:
[📝 Мои курсы] → /seller/items
[📥 Заявки] → /seller/leads
[⭐ Отзывы] → /seller/reviews
[👤 Профиль] → /seller/profile
[👥 Сотрудники] → /seller/staff
UC-02 — Альтернативные потоки
2a. Ошибка загрузки метрик (API вернул 5xx):
UI-реакция:
→ Карточки метрик: каждая показывает "—" вместо числа
→ Под каждой карточкой (или общий баннер): "Не удалось загрузить данные. Обновить"
→ Ссылка "Обновить" перезапрашивает stats API
→ Остальные блоки страницы загружаются независимо (не блокируют друг друга)
2b. Долгая загрузка метрик (> 2 секунды):
UI-реакция:
→ Карточки метрик: скелетон-заглушки (серые прямоугольники) пока данные загружаются
→ После загрузки — плавная замена скелетона на реальные данные
2c. Пользователь переключает Period Selector слишком быстро:
Поведение:
→ Предыдущий запрос отменяется (AbortController)
→ Загружаются данные для последнего выбранного периода
→ Loading state показывается только для метрик, не для всей страницы
2d. Нет данных за выбранный период (например, продавец выбрал "Сегодня", но зарегистрировался вчера):
UI-реакция:
→ Карточки: показывают "0" (не ошибку)
→ Подсказка под метрикой: "Нет данных за этот период"
→ Тренд: не показывается (нет сравнения)
UC-03: Продавец просматривает список айтемов с бейджами статусов
Актор: Seller (Owner / Admin CRM / Manager) Предусловие: Продавец авторизован Триггер: Продавец нажимает "Мои курсы" в дашборде или в навигации → /seller/items
Полный поток:
[Точка входа]
→ Продавец на /seller нажимает "Управлять" в карточке "Активных курсов"
→ ИЛИ: в левой навигации нажимает "Курсы"
→ Открывается /seller/items
─────────────────────────────────────────────────────────
СТРАНИЦА /seller/items
─────────────────────────────────────────────────────────
→ Хедер: "Мои курсы ({total})" + кнопка "+ Создать курс"
→ Список/таблица айтемов:
Каждая карточка или строка содержит:
- Обложка (thumbnail, 80×60px)
- Название айтема
- Предмет/направление
- Статус-бейдж (цветной):
[Активен] — зелёный
[Черновик] — серый
[На проверке] — синий/жёлтый
[Отклонён] — красный
[Нужна доработка] — оранжевый
[Скрыт] — серый с иконкой 👁🗨
[Архив] — светло-серый
- Дата создания / последнего изменения
- Кол-во лидов (total)
- Переключатель видимости (toggle, только для active айтемов)
- Меню "⋯": Редактировать / Скрыть / Удалить
→ Состояние загрузки: скелетоны строк
→ Пустое состояние (нет айтемов):
"У вас пока нет курсов."
Кнопка: "+ Создать первый курс"
UC-03 — Альтернативные потоки
3a. Ошибка загрузки списка айтемов:
UI-реакция:
→ Вместо списка: сообщение "Не удалось загрузить список курсов."
→ Кнопка "Попробовать снова"
→ Если ошибка повторяется: ссылка "Написать в поддержку"
3b. Продавец нажимает "Редактировать" на pending-айтеме:
UI-реакция: (из Spec 02, UC-02-a)
→ Форма открывается в режиме только для чтения
→ Баннер вверху: "Курс находится на проверке. Редактирование недоступно."
→ Кнопка: "Отозвать с проверки" (возвращает в draft)
UC-04: Продавец фильтрует список айтемов по статусу
Актор: Seller (Owner / Admin CRM / Manager) Предусловие: Продавец на странице /seller/items с несколькими айтемами разных статусов Триггер: Продавец хочет увидеть только определённые айтемы
Полный поток:
[Точка входа]
→ На /seller/items продавец видит таб-фильтры:
─────────────────────────────────────────────────────────
ФИЛЬТРЫ ПО СТАТУСУ
─────────────────────────────────────────────────────────
→ Табы с счётчиками:
[Все (18)] [Активные (11)] [Черновики (3)] [На проверке (2)] [Требуют внимания (2)]
"Требуют внимания" = объединяет: rejected + revision_required
→ При нажатии на таб:
- URL обновляется: /seller/items?status=active
- Список фильтруется на стороне клиента (если все айтемы уже загружены)
- ИЛИ: новый запрос к API с параметром ?status=...
→ Дополнительный фильтр (dropdown): "Сортировка"
- Новые сначала (default)
- Старые сначала
- По количеству заявок
- По количеству просмотров (v1.5)
→ Поиск (text input): по названию курса
UC-04 — Альтернативные потоки
4a. Фильтр применён, нет айтемов с таким статусом:
UI-реакция:
→ Пустое состояние для конкретного фильтра:
Для "Активные": "У вас нет активных курсов. Отправьте черновик на проверку."
Для "На проверке": "Нет курсов на проверке."
Для "Требуют внимания": "Всё в порядке — нет курсов, требующих исправления ✓"
4b. Поиск не дал результатов:
UI-реакция:
→ "По запросу «{текст}» ничего не найдено."
→ Кнопка: "Сбросить поиск"
UC-05: Продавец видит уведомления о pending/rejected айтемах
Актор: Seller (Owner / Admin CRM)
Предусловие: У продавца есть айтемы в статусах требующих внимания: rejected или revision_required
Триггер: Продавец открывает /seller (главный дашборд)
Полный поток:
[Точка входа]
→ Продавец открывает /seller
→ Система обнаруживает айтемы со статусами rejected / revision_required
─────────────────────────────────────────────────────────
БАННЕР УВЕДОМЛЕНИЯ (под хедером, над метриками)
─────────────────────────────────────────────────────────
СЛУЧАЙ А: есть rejected-айтемы:
→ Красный баннер (warning):
"⚠ {count} курс(а/ов) отклонён(ы) после проверки."
Краткий текст: "Просмотрите причину и исправьте."
Кнопка: "Посмотреть отклонённые" → /seller/items?status=rejected
Иконка закрытия (X) — скрывает баннер до следующего входа
СЛУЧАЙ Б: есть revision_required-айтемы:
→ Оранжевый баннер:
"📝 {count} курс(а/ов) отправлен(ы) на доработку."
Кнопка: "Исправить" → /seller/items?status=revision_required
СЛУЧАЙ В: одновременно есть и rejected, и revision_required:
→ Показываются оба баннера, каждый своей строкой (или объединённый):
"⚠ 1 курс отклонён · 2 курса ожидают доработки"
Кнопка: "Посмотреть" → /seller/items?status=rejected,revision_required
─────────────────────────────────────────────────────────
НА СТРАНИЦЕ /seller/items (детальный вид)
─────────────────────────────────────────────────────────
→ Карточка rejected-айтема:
Статус-бейдж [Отклонён] (красный)
Раскрывающийся блок с комментарием от верификатора:
"Причина: {текст комментария}"
Кнопка: "Редактировать и отправить повторно"
→ Карточка revision_required-айтема:
Статус-бейдж [Нужна доработка] (оранжевый)
Раскрывающийся блок:
"Что нужно исправить: {текст комментария}"
Кнопка: "Исправить"
UC-05 — Альтернативные потоки
5a. Продавец закрыл баннер (нажал X) и вышел из кабинета:
Поведение:
→ Состояние "скрыт" хранится в localStorage или sessionStorage
→ При следующем входе: баннер снова показывается (если статус не изменился)
→ Баннер исчезает перманентно только когда айтем выходит из rejected/revision_required
5b. Айтем в статусе pending более 3 дней:
UI-реакция:
→ На карточке айтема появляется подсказка:
"На проверке более 3 дней. Обычно это занимает 1–2 дня."
Ссылка: "Написать в поддержку" → support@qadam.uz
(Это информационная подсказка, не ошибка)
4. Бизнес-правила и валидации
Правила метрик и подсчёта
| Метрика | Как считается | Зависит от Period Selector |
|---|---|---|
| Активных курсов | COUNT(items WHERE moderation_status=active AND item_isvisible=true AND seller_id=X) | ❌ (всегда total) |
| Просмотры | COUNT(item_views WHERE seller_id=X AND viewed_at IN period) | ✅ |
| Заявки (лиды) | COUNT(leads WHERE seller_id=X AND created_at IN period) | ✅ |
| Отзывы | COUNT(reviews WHERE seller_id=X AND review_status=active) | ❌ (всегда total) |
| Средний рейтинг | AVG(reviews.rating WHERE seller_id=X AND review_status=active) | ❌ (всегда total) |
Правила отображения периодов
| Period | Диапазон | Тренд: сравнивается с |
|---|---|---|
| Сегодня | today 00:00 → now() | Вчера (тот же промежуток) |
| Неделя | last 7 days | Предыдущие 7 дней |
| Месяц | last 30 days | Предыдущие 30 дней |
| Всё время | created_at → now() | Нет тренда |
Бизнес-правила
- Welcome-состояние: Показывается если у продавца нет НИ ОДНОГО айтема любого статуса. Наличие хотя бы одного черновика = не welcome-состояние.
- Независимость блоков: Ошибка загрузки одного блока (метрик, списка курсов, заявок) не ломает другие. Каждый блок загружается независимо.
- Кэш метрик: Метрики кэшируются на 5 минут (TTL). При переключении Period Selector — данные перезапрашиваются.
- Тренд не показывается при нулях: Если за текущий и предыдущий период значения = 0, тренд не отображается (нет деления на ноль).
- Айтем виден в списке /seller/items всегда: Независимо от статуса, все айтемы видны продавцу в его кабинете. Скрытые от публики (hidden, pending etc.) — с соответствующими бейджами.
Матрица доступа к дашборду
| Блок | Owner | Admin CRM | Manager | Teacher |
|---|---|---|---|---|
| Метрики (просмотры, лиды, отзывы) | ✅ | ✅ | ✅ | ❌ |
| Список последних заявок (preview) | ✅ | ✅ | ✅ | ❌ |
| Список моих курсов | ✅ | ✅ | ✅ | ✅ (только свои) |
| Баннеры об отклонённых айтемах | ✅ | ✅ | ✅ | ❌ |
| Кнопка "+ Создать курс" | ✅ | ✅ | ✅ | ❌ |
5. Модель данных
Используются существующие сущности: Item, Lead, Review. Новые: ItemView (просмотры).
SellerDashboardStats (агрегированная DTO, не модель в БД)
| Атрибут | Тип | Описание |
|---|---|---|
| active_items_count | int | Активные айтемы в каталоге |
| views_count | int | Уникальные просмотры за период |
| views_trend | float? | % изменение vs предыдущий период |
| leads_count | int | Лиды за период |
| leads_trend | float? | % изменение vs предыдущий период |
| reviews_count | int | Всего активных отзывов |
| average_rating | float? | Средний рейтинг (null если нет отзывов) |
| period | PeriodType | today / week / month / all_time |
ItemView (просмотры айтема, новая сущность)
| Атрибут | Тип | Описание |
|---|---|---|
| view_id | UUID | PK |
| item_id | UUID FK | → Item |
| seller_id | UUID FK | → Seller (денормализовано для быстрых агрегатов) |
| viewer_fingerprint | string? | Анонимный идентификатор (cookie/IP hash) для дедупликации |
| buyer_id | UUID FK? | → Buyer (если авторизован) |
| viewed_at | DateTime |
Item (существующая, расширение для дашборда)
| Атрибут | Тип | Описание |
|---|---|---|
| item_id | UUID | PK (существующий) |
| seller_id | UUID FK | → Seller (существующий) |
| item_name | string | (существующий) |
| moderation_status | ItemStatus | draft/pending/active/rejected/revision_required/archived |
| item_isvisible | boolean | (существующий) |
| moderation_comment | text? | Комментарий от верификатора — виден продавцу |
| cover_url | string? | URL обложки для thumbnail в списке |
Lead (существующая, для счётчика в дашборде — детали в Spec 09)
| Атрибут | Тип | Описание |
|---|---|---|
| lead_id | UUID | PK |
| seller_id | UUID FK | → Seller (денормализовано для агрегатов) |
| item_id | UUID FK | → Item |
| created_at | DateTime | Дата заявки (для Period Selector) |
Review (существующая — детали в Spec 14, для счётчика в дашборде)
| Атрибут | Тип | Описание |
|---|---|---|
| review_id | UUID | PK |
| seller_id | UUID FK | → Seller (денормализовано) |
| item_id | UUID FK | → Item |
| rating | int | 1–5 |
| review_status | ReviewStatus | published / pending_moderation / rejected |
| created_at | DateTime |
6. Технические контракты
6.1 Prisma Schema (добавления)
enum PeriodType {
today
week
month
all_time
}
model ItemView {
view_id String @id @default(uuid())
item_id String
seller_id String // денормализация для быстрых агрегатов
viewer_fingerprint String? // cookie/IP hash для дедупликации
buyer_id String?
viewed_at DateTime @default(now())
item Item @relation(fields: [item_id], references: [item_id])
seller Seller @relation(fields: [seller_id], references: [seller_id])
@@index([seller_id, viewed_at]) // для агрегатов по периоду
@@index([item_id, viewed_at])
}
6.2 TypeScript DTO
// ─── Period Selector ──────────────────────────────────────────────────────
export type PeriodType = 'today' | 'week' | 'month' | 'all_time'
// ─── Dashboard Stats ──────────────────────────────────────────────────────
export interface SellerDashboardStatsResponse {
period: PeriodType
active_items_count: number // не зависит от периода
views: {
count: number
trend_pct: number | null // null если нет данных для сравнения
trend_direction: 'up' | 'down' | 'neutral' | null
}
leads: {
count: number
trend_pct: number | null
trend_direction: 'up' | 'down' | 'neutral' | null
}
reviews: {
count: number // не зависит от периода
average_rating: number | null // null если нет отзывов
}
}
// ─── Items List (для /seller/items) ──────────────────────────────────────
export interface SellerItemCardResponse {
item_id: string
item_name: string
item_slug: string
cover_url: string | null
subject: SubjectShortDto
moderation_status: ItemStatus
item_isvisible: boolean
moderation_comment: string | null // показывается при rejected/revision_required
leads_count: number // total лидов по этому айтему
created_at: string
updated_at: string
}
export interface SellerItemsListResponse {
items: SellerItemCardResponse[]
total: number
counts_by_status: {
all: number
active: number
draft: number
pending: number
needs_attention: number // rejected + revision_required
hidden: number // active + item_isvisible=false
archived: number
}
}
// ─── Dashboard Recent Leads preview ──────────────────────────────────────
export interface LeadPreviewDto {
lead_id: string
buyer_name: string // имя покупателя (или "Аноним")
item_name: string
item_id: string
created_at: string
}
// ─── Full Dashboard Page Response ────────────────────────────────────────
export interface SellerDashboardPageResponse {
seller: {
seller_id: string
org_name: string // или first_name для individual_contributor
seller_type: SellerType
account_status: AccountStatus
}
stats: SellerDashboardStatsResponse
recent_items: SellerItemCardResponse[] // последние 5
recent_leads: LeadPreviewDto[] // последние 5 необработанных
has_items: boolean // false = показать welcome-состояние
attention_required: {
rejected_count: number
revision_required_count: number
}
}
// ─── Query Params ─────────────────────────────────────────────────────────
export class GetSellerItemsQueryDto {
@IsOptional()
@IsEnum(['all', 'active', 'draft', 'pending', 'rejected', 'revision_required', 'hidden', 'archived'])
status?: string
@IsOptional() @IsString() @MaxLength(200)
search?: string
@IsOptional()
@IsEnum(['newest', 'oldest', 'most_leads'])
sort?: string // default: 'newest'
@IsOptional() @IsInt() @Min(1) @Max(100)
limit?: number // default: 50
@IsOptional() @IsInt() @Min(1)
page?: number // default: 1
}
export class GetDashboardStatsQueryDto {
@IsEnum(['today', 'week', 'month', 'all_time'])
period: PeriodType // default: 'week'
}
6.3 API Endpoints
────────────────────────────────────────────────────────────────
SELLER DASHBOARD
────────────────────────────────────────────────────────────────
GET /api/seller/dashboard
Auth: Bearer (seller)
Query: ?period=today|week|month|all_time (default: week)
→ 200: SellerDashboardPageResponse
→ 401: { error: 'UNAUTHORIZED' }
→ 403: { error: 'INSUFFICIENT_ROLE' } // Teacher не имеет доступа к общим метрикам
Примечание: Этот эндпоинт агрегирует данные из нескольких таблиц.
Реализуется через набор COUNT-запросов с индексами по seller_id.
Не использует SAA-слой на MVP — прямые SQL-агрегаты.
────────────────────────────────────────────────────────────────
SELLER DASHBOARD STATS (отдельный эндпоинт для Period Selector)
────────────────────────────────────────────────────────────────
GET /api/seller/dashboard/stats
Auth: Bearer (seller)
Query: ?period=today|week|month|all_time
→ 200: SellerDashboardStatsResponse
→ 401: { error: 'UNAUTHORIZED' }
Примечание: Вызывается при переключении Period Selector без перезагрузки страницы.
Кэшируется на стороне сервера: TTL 5 минут per (seller_id, period).
────────────────────────────────────────────────────────────────
SELLER ITEMS LIST
────────────────────────────────────────────────────────────────
GET /api/seller/items
Auth: Bearer (seller)
Query: GetSellerItemsQueryDto
→ 200: SellerItemsListResponse
→ 401: { error: 'UNAUTHORIZED' }
Примечание:
- Для роли Teacher: автоматически фильтрует по item_performer_links.staff_id = текущий teacher
- Для Owner/Admin CRM/Manager: все айтемы организации
- Включает ВСЕ статусы (draft, pending, etc.) — в отличие от публичного каталога
- items_count_by_status вычисляется одним агрегатным запросом
────────────────────────────────────────────────────────────────
SELLER ITEMS (управление — детали в Spec 02)
────────────────────────────────────────────────────────────────
GET /api/seller/items/:item_id
Auth: Bearer (seller)
→ 200: SellerItemCardResponse (расширенный, с полным описанием)
→ 403: { error: 'FORBIDDEN' } // чужой айтем
→ 404: { error: 'ITEM_NOT_FOUND' }
PATCH /api/seller/items/:item_id/visibility
Auth: Bearer (seller: owner | admin_crm | manager)
Body: { item_isvisible: boolean }
→ 200: { item_id: string, item_isvisible: boolean }
→ 400: { error: 'ITEM_NOT_ACTIVE', message: 'Управление видимостью доступно только для активных курсов.' }
→ 403: { error: 'INSUFFICIENT_ROLE' }
────────────────────────────────────────────────────────────────
ITEM VIEWS (трекинг просмотров — вызывается с публичных страниц)
────────────────────────────────────────────────────────────────
POST /api/items/:item_id/view
Auth: Public (опционально — если авторизован, передаём buyer_id)
Body: { viewer_fingerprint?: string }
→ 200: { recorded: true }
→ 404: { error: 'ITEM_NOT_FOUND' }
Примечание:
- Не записываем просмотр если viewer_fingerprint совпадает с записью за последние 24 часа
(дедупликация уникальных просмотров)
- Записывает просмотр только для active + isvisible=true айтемов
- Rate limit: не более 10 req/min с одного IP (защита от накрутки)
7. Edge Cases и обработка ошибок
| Сценарий | Поведение |
|---|---|
| Продавец заблокирован (account_status=blocked) — открывает /seller | Редирект на /seller/blocked. Страница: "Ваш аккаунт заблокирован. {причина}. Обратитесь: support@qadam.uz" |
| Продавец under_review — открывает /seller | Дашборд доступен только для чтения. Баннер вверху: "Ваш аккаунт проходит проверку. Создание курсов временно недоступно." Кнопка "+ Создать курс" скрыта. |
| Все айтемы удалены (archived) — item_count=0, но has_items=false | Показывается welcome-состояние (аналог нового продавца) |
| Деление на ноль в расчёте тренда (предыдущий период = 0, текущий > 0) | trend_pct = null. UI показывает ↑ (рост) без процентного значения: "Новые данные" |
| Деление на ноль (предыдущий > 0, текущий = 0) | trend_pct показывается как -100% |
| Продавец меняет Period Selector, пока идёт предыдущий запрос | AbortController отменяет предыдущий запрос. Новый запрос с актуальным периодом |
| ItemView.viewer_fingerprint не передан (старый браузер) | Просмотр записывается без дедупликации. Счётчик может быть немного завышен. |
| Teacher открывает /seller | Показывается ограниченный дашборд: только список айтемов в которых он участвует. Метрики скрыты. |
| Продавец открывает /seller/items, в данных > 1000 айтемов | Пагинация: limit=50, page=N. Показывается pagination controls. |
| Айтем переходит из rejected в pending (продавец исправил) — обновление счётчика | attention_required.rejected_count уменьшается. Баннер на дашборде обновляется при следующем входе (или при обновлении страницы). |
8. TBD / Сознательно опущено
| Тема | Статус | Примечание |
|---|---|---|
| SAA-слой для аналитики | Исключено из MVP | На MVP используются прямые COUNT-запросы к PostgreSQL. SAA-слой (Snowflake/ClickHouse) — v1.5. |
| Real-time обновления метрик (WebSocket / SSE) | Исключено из MVP | Метрики обновляются только при переключении Period Selector или обновлении страницы. Live-обновления — v1.0. |
| График динамики лидов и просмотров | Исключено из MVP | На MVP только цифры и тренд. Визуализация графиков (Chart.js / Recharts) — v1.5. |
| Детальная аналитика по каждому курсу | Исключено из MVP | Переход из карточки курса в детальную аналитику — v1.5. |
| Экспорт данных (CSV/Excel) | Исключено из MVP | |
| Email-дайджест метрик (еженедельный отчёт) | Исключено из MVP | v1.0 |
| Дедупликация просмотров (уникальные vs. total) | Частично | На MVP: viewer_fingerprint для 24-часовой дедупликации. Более точная методология — TBD. |
| Аналитика по каналам привлечения (UTM) | Вне скоупа | Не планируется на MVP. |
| Сравнение с другими продавцами (benchmark) | Исключено из MVP | Конкурентная аналитика — v2.0. |
| Уведомления о новых заявках на дашборде (real-time badge) | TBD | В навигации: счётчик непрочитанных лидов. Детали — Spec 09 и Spec 10. |
| Period Selector: кастомный диапазон дат | Исключено из MVP | Только фиксированные периоды. Кастомный диапазон — v1.5. |
| Метрика "Конверсия просмотры → заявки" | Исключено из MVP | Вычисляемая метрика. Полезна, но не критична для MVP. v1.5. |
Зависимости
| Модуль | Связь |
|---|---|
| Spec 01 (Seller Profile) | Seller.org_name и seller_type используются в хедере дашборда |
| Spec 02 (Item Management) | SellerItemCardResponse — карточка айтема с moderation_status и item_isvisible |
| Spec 03 (Staff Management) | Роль Teacher ограничивает отображение только своих айтемов |
| Spec 04 (Admin Moderation) | moderation_status и moderation_comment — результат модерации, отображается в /seller/items |
| Spec 09 (Lead Management) | Lead.seller_id + Lead.created_at — основа для счётчика заявок за период |
| Spec 10 (Notifications) | Уведомления о новых лидах и результатах модерации |
| Spec 14 (Reviews) | Review.rating + Review.review_status — основа для счётчика отзывов и среднего рейтинга |