Qadam Roadmap
проектdocs/Agents/specs/2026-03-24-mvp-spec-11-seller-dashboard.md

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()Нет тренда

Бизнес-правила

  1. Welcome-состояние: Показывается если у продавца нет НИ ОДНОГО айтема любого статуса. Наличие хотя бы одного черновика = не welcome-состояние.
  2. Независимость блоков: Ошибка загрузки одного блока (метрик, списка курсов, заявок) не ломает другие. Каждый блок загружается независимо.
  3. Кэш метрик: Метрики кэшируются на 5 минут (TTL). При переключении Period Selector — данные перезапрашиваются.
  4. Тренд не показывается при нулях: Если за текущий и предыдущий период значения = 0, тренд не отображается (нет деления на ноль).
  5. Айтем виден в списке /seller/items всегда: Независимо от статуса, все айтемы видны продавцу в его кабинете. Скрытые от публики (hidden, pending etc.) — с соответствующими бейджами.

Матрица доступа к дашборду

БлокOwnerAdmin CRMManagerTeacher
Метрики (просмотры, лиды, отзывы)
Список последних заявок (preview)
Список моих курсов✅ (только свои)
Баннеры об отклонённых айтемах
Кнопка "+ Создать курс"

5. Модель данных

Используются существующие сущности: Item, Lead, Review. Новые: ItemView (просмотры).

SellerDashboardStats (агрегированная DTO, не модель в БД)

АтрибутТипОписание
active_items_countintАктивные айтемы в каталоге
views_countintУникальные просмотры за период
views_trendfloat?% изменение vs предыдущий период
leads_countintЛиды за период
leads_trendfloat?% изменение vs предыдущий период
reviews_countintВсего активных отзывов
average_ratingfloat?Средний рейтинг (null если нет отзывов)
periodPeriodTypetoday / week / month / all_time

ItemView (просмотры айтема, новая сущность)

АтрибутТипОписание
view_idUUIDPK
item_idUUID FK→ Item
seller_idUUID FK→ Seller (денормализовано для быстрых агрегатов)
viewer_fingerprintstring?Анонимный идентификатор (cookie/IP hash) для дедупликации
buyer_idUUID FK?→ Buyer (если авторизован)
viewed_atDateTime

Item (существующая, расширение для дашборда)

АтрибутТипОписание
item_idUUIDPK (существующий)
seller_idUUID FK→ Seller (существующий)
item_namestring(существующий)
moderation_statusItemStatusdraft/pending/active/rejected/revision_required/archived
item_isvisibleboolean(существующий)
moderation_commenttext?Комментарий от верификатора — виден продавцу
cover_urlstring?URL обложки для thumbnail в списке

Lead (существующая, для счётчика в дашборде — детали в Spec 09)

АтрибутТипОписание
lead_idUUIDPK
seller_idUUID FK→ Seller (денормализовано для агрегатов)
item_idUUID FK→ Item
created_atDateTimeДата заявки (для Period Selector)

Review (существующая — детали в Spec 14, для счётчика в дашборде)

АтрибутТипОписание
review_idUUIDPK
seller_idUUID FK→ Seller (денормализовано)
item_idUUID FK→ Item
ratingint1–5
review_statusReviewStatuspublished / pending_moderation / rejected
created_atDateTime

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-дайджест метрик (еженедельный отчёт)Исключено из MVPv1.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 — основа для счётчика отзывов и среднего рейтинга