# MVP Spec 03 — Staff Management

## Паспорт документа

- Статус документа: working spec
- Актуально на: 28 марта 2026 года
- Владелец: backend/platform-команда
- Пересмотр: перед реализацией, при изменении domain rules или при смене source-of-truth по контракту
- Область применения: детальные feature-спеки для product backlog и реализации MVP/v1 направлений
- Связанные документы:
  - [Product roadmap и delivery checklist](../product-roadmap.md)
  - [Roadmap](../../project/roadmap.md)
  - [Карта API-маршрутов](../../architecture/api-routes.md)

> Version: MVP · Priority: P0 · Phase: A (Supply)
> Status: Draft v1

---

## 1. Контекст и цель

Сотрудники (Staff) — люди, которые работают в образовательной организации. В MVP это две группы:

1. **Административный персонал** — люди, которые управляют кабинетом продавца (обрабатывают лиды, редактируют курсы). Не отображаются публично.
2. **Преподаватели / Перформеры** — люди, которые ведут занятия. Отображаются на карточке курса и в профиле школы. Являются частью доверительной информации для байера.

**Цель модуля:** дать владельцу школы инструмент для создания команды с разными уровнями доступа, и предоставить публичный профиль каждого преподавателя как часть trust-сигнала платформы.

**Что не входит в этот модуль:**
- Управление расписанием и рабочими часами → Spec v1.0 (CRM)
- Расчёт зарплат, KPI → Spec v1.5 (Analytics)
- Роли Manager и Teacher в полном объёме → Spec v1.0 (CRM Roles)
- Привязка сотрудника к конкретному айтему → Spec 02 (ItemPerformerLink)

---

## 2. Роли пользователей

| Роль | Действия |
|------|---------|
| **Seller Owner** | Полный CRUD сотрудников, изменение ролей, блокировка |
| **Seller Admin CRM** | Создание и редактирование сотрудников, блокировка (без изменения Owner) |
| **Seller Manager** | Только просмотр списка сотрудников |
| **Seller Teacher** | Просмотр собственного профиля и его редактирование (bio, фото) |

---

## 3. Use Cases

---

### UC-01: Создание сотрудника (Happy Path)

**Актор:** Seller Owner / Admin CRM
**Предусловие:** Продавец авторизован
**Триггер:** Нажимает "+ Добавить сотрудника" в /seller/staff

**Полный поток:**

```
[Точка входа]
→ Продавец в левом меню кабинета нажимает "Сотрудники"
→ Открывается /seller/staff
→ Нажимает "+ Добавить сотрудника"
→ Открывается модал или страница /seller/staff/new

─────────────────────────────────────────────────────────
ФОРМА СОЗДАНИЯ СОТРУДНИКА
─────────────────────────────────────────────────────────
Блок 1 — Основные данные:
    - Имя * (2–50 символов)
    - Фамилия * (2–50 символов)
    - Телефон * (формат +998XXXXXXXXX)
    - Email * (валидный email)

Блок 2 — Роль в организации *:
    Радио-кнопки с описанием:

    ◉ Администратор
       Может редактировать курсы, обрабатывать заявки, управлять персоналом.
       Не отображается публично.

    ○ Преподаватель / Тренер
       Ведёт занятия. Отображается в карточках курсов и профиле школы.
       Может входить в кабинет только в v1.0 (сейчас — только публичный профиль).

Блок 3 — Публичный профиль преподавателя
    (показывается только если выбрана роль "Преподаватель / Тренер"):
    - Специализация (до 100 символов), напр. "Преподаватель английского"
    - Опыт работы (число + "лет"), необязательно
    - Bio / описание (до 1000 символов), необязательно
    - Фото (JPG/PNG/WebP, max 5 МБ, min 200×200px), необязательно

→ Нажимает "Создать"
→ Система создаёт:
    Account { account_type: SELLER_STAFF, account_status: active }
    SellerStaff { seller_id, role: admin | teacher, staff_status: active }
    PerformerProfile (если role = teacher)
→ На указанный email отправляется письмо-приглашение:
    "Вы добавлены в команду {org_name} на платформе Qadam.
    Войдите по ссылке: [ссылка на /login]
    Ваш логин: {email}, временный пароль: {temp_password}"
→ Toast: "Сотрудник добавлен. Приглашение отправлено на {email}."
→ Новый сотрудник появляется в списке /seller/staff
```

---

### UC-01 — Альтернативные потоки и обработка ошибок

**1a. Email уже зарегистрирован в системе (у другого аккаунта):**
```
UI-реакция:
→ Поле email: красная обводка + ⚠
→ Под полем: "Пользователь с таким email уже зарегистрирован на платформе."
→ Подсказка: "Если это ваш сотрудник — попросите его войти и связаться с поддержкой для привязки к вашей организации."
```

**1b. Телефон уже зарегистрирован:**
```
UI-реакция:
→ Поле телефона: красная обводка + ⚠
→ Под полем: "Этот номер уже используется другим аккаунтом."
```

**1c. Обязательное поле пустое:**
```
UI-реакция:
→ Красная обводка + ⚠ + "Это поле обязательно для заполнения"
→ Форма не отправляется
```

**1d. Загрузка фото преподавателя — файл > 5 МБ:**
```
UI-реакция:
→ Под полем фото: "Файл слишком большой. Максимум 5 МБ."
→ Файл не принимается, поле сбрасывается
```

**1e. Ошибка отправки email-приглашения (SMTP недоступен):**
```
Поведение:
→ Сотрудник СОЗДАЁТСЯ (аккаунт в БД сохранён)
→ Toast (жёлтый, предупреждение): "Сотрудник добавлен, но письмо-приглашение не было отправлено.
  Передайте логин и временный пароль вручную или повторите отправку."
→ В карточке сотрудника: кнопка "Отправить приглашение повторно"
```

**1f. Технический сбой при сохранении:**
```
UI-реакция:
→ Toast (красный): "Не удалось создать сотрудника. Данные сохранены в форме. Попробуйте снова."
→ Форма не закрывается, данные не сбрасываются
```

---

### UC-02: Просмотр списка сотрудников

**Актор:** Seller (Owner / Admin CRM / Manager)
**Предусловие:** Продавец авторизован
**Триггер:** Открывает /seller/staff

**Полный поток:**

```
→ Список сотрудников в виде карточек или таблицы:
  Каждая запись: фото (или аватар-заглушка), имя, роль, статус, email, телефон
  Бейджи ролей: [Администратор] / [Преподаватель]
  Бейджи статусов: [Активен] / [Заблокирован]

→ Действия на карточке (для Owner/Admin CRM):
  ✏ Редактировать
  🔒 Заблокировать / Разблокировать
  🗑 Удалить (только Owner)

→ Фильтр по роли: Все / Администраторы / Преподаватели
→ Пустое состояние (нет сотрудников): иллюстрация + текст
  "У вас пока нет сотрудников. Добавьте первого."
  Кнопка "+ Добавить сотрудника"
```

---

### UC-03: Редактирование профиля сотрудника

**Актор:** Seller Owner / Admin CRM (любого сотрудника) / Teacher (только свой профиль)
**Предусловие:** Сотрудник существует

**Полный поток:**

```
→ В /seller/staff нажимает "Редактировать" на карточке сотрудника
→ Открывается форма с предзаполненными данными
→ Поля доступные для редактирования:

  Для Owner / Admin CRM:
    - Имя, фамилия, телефон, email
    - Роль (смена роли — только Owner)
    - Специализация, опыт, bio (если Преподаватель)
    - Фото (если Преподаватель)

  Для Teacher (свой профиль):
    - Специализация, bio, фото (только публичный профиль)
    - НЕ может: менять email/телефон/роль

→ Нажимает "Сохранить"
→ Toast: "Профиль обновлён ✓"
→ Изменения публичного профиля преподавателя сразу отражаются
  на страницах айтемов и публичном профиле школы
```

**Альтернативные потоки:**

**a. Owner пытается изменить роль единственного Owner:**
```
UI-реакция:
→ Dropdown роли заблокирован (disabled)
→ Tooltip: "Нельзя снять роль Owner с последнего владельца."
```

**b. Admin CRM пытается редактировать Owner-аккаунт:**
```
UI-реакция:
→ Кнопка "Редактировать" недоступна для Owner-карточки
→ Tooltip: "Только владелец может изменять свои данные."
```

---

### UC-04: Блокировка / разблокировка сотрудника

**Актор:** Seller Owner / Admin CRM
**Предусловие:** Сотрудник существует и активен

**Блокировка:**
```
→ Нажимает "Заблокировать" на карточке сотрудника
→ Диалог: "Заблокировать {имя}?
  Сотрудник потеряет доступ к кабинету. Его профиль преподавателя
  будет скрыт с публичных страниц."
→ [Отмена] [Заблокировать]
→ SellerStaff.staff_status = blocked
→ Account.account_status = blocked (для этого staff аккаунта)
→ Профиль преподавателя скрывается с публичных страниц
→ Toast: "{имя} заблокирован."
```

**Разблокировка:**
```
→ Нажимает "Разблокировать"
→ Без диалога: мгновенное действие
→ staff_status = active, account_status = active
→ Профиль преподавателя снова виден публично (если был teacher)
→ Toast: "{имя} разблокирован."
```

**Альтернативный поток:**

**a. Попытка заблокировать единственного Owner:**
```
UI-реакция:
→ Кнопка "Заблокировать" недоступна
→ Tooltip: "Нельзя заблокировать единственного владельца организации."
```

---

### UC-05: Удаление сотрудника

**Актор:** Seller Owner (только)
**Предусловие:** Сотрудник существует

**Полный поток:**

```
→ Нажимает "Удалить" на карточке сотрудника
→ Диалог:
  "Удалить {имя} из организации?
  Сотрудник потеряет доступ к кабинету. Если он привязан к курсам
  как преподаватель — его профиль будет удалён с этих курсов.
  Это действие нельзя отменить."
→ [Отмена] [Удалить]
→ Выполняется:
    SellerStaff.staff_status = archived (soft delete)
    Account.account_status = blocked
    ItemPerformerLink: все записи с этим staff_id удаляются
→ Toast: "{имя} удалён из организации."
```

**Альтернативные потоки:**

**a. Попытка удалить единственного Owner:**
```
UI-реакция:
→ Кнопка "Удалить" недоступна
→ Tooltip: "Нельзя удалить единственного владельца."
```

**b. Сотрудник привязан к активным айтемам как преподаватель:**
```
UI-реакция:
→ Диалог добавляет предупреждение:
  "Этот преподаватель указан в {N} курсах: {название1}, {название2}...
  После удаления его профиль исчезнет с этих курсов."
→ [Отмена] [Всё равно удалить]
```

---

### UC-06: Первый вход сотрудника по приглашению

**Актор:** Новый сотрудник (SellerStaff)
**Предусловие:** Owner создал сотрудника, письмо с приглашением отправлено

**Полный поток:**

```
→ Сотрудник получает email с ссылкой и временным паролем
→ Открывает /login
→ Вводит email + временный пароль
→ Успешный вход
→ Система обнаруживает что пароль временный (is_temp_password = true)
→ Принудительный редирект на /change-password:
  "Пожалуйста, установите постоянный пароль для вашего аккаунта."
  Поля: Новый пароль + Подтверждение
→ После смены пароля: редирект в /seller (кабинет с ограниченным меню)
→ Toast: "Пароль установлен. Добро пожаловать!"
```

**Меню Staff в кабинете (MVP — ограниченное):**
- Admin CRM: видит те же разделы что Owner (кроме биллинга)
- Teacher (MVP): видит только "Мой профиль" и список курсов где участвует

---

## 4. Бизнес-правила и валидации

### Валидация полей

| Поле | Правило | Ошибка пользователю |
|------|---------|-------------------|
| Имя / Фамилия | 2–50 символов | "Имя: от 2 до 50 символов" |
| Телефон | +998XXXXXXXXX, уникальный в системе | "Введите номер в формате +998XXXXXXXXX" |
| Email | Валидный, уникальный в системе | "Введите корректный email" |
| Специализация | до 100 символов | "Максимум 100 символов" |
| Опыт работы | 0–70 лет | "Укажите корректное значение (0–70 лет)" |
| Bio | до 1000 символов | "Bio: максимум 1000 символов" |
| Фото | JPG/PNG/WebP, max 5 МБ, min 200×200px | "Фото: max 5 МБ, min 200×200px, JPG/PNG/WebP" |

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

1. **Owner — единственная незаменимая роль.** Нельзя удалить, заблокировать или сменить роль единственного Owner. Минимум 1 Owner всегда.
2. **Роль Owner создаётся при регистрации.** Seller-аккаунт = Owner своей организации. Нельзя передать Owner через интерфейс на MVP (только через поддержку).
3. **SELLER_STAFF — отдельный AccountType.** У каждого сотрудника свой Account. Это не роль внутри seller_account, это отдельный аккаунт с ограниченным доступом.
4. **Публичный профиль — только Teacher.** Только сотрудники с ролью `teacher` имеют PerformerProfile и отображаются публично. Admin и Manager — только внутренние.
5. **Блокировка скрывает профиль.** При `staff_status = blocked` PerformerProfile не возвращается в публичных API.
6. **Удаление — soft delete.** `staff_status = archived`. Физически не удаляется — история лидов сохраняется.
7. **Временный пароль.** При создании сотрудника генерируется временный пароль (8 символов, случайный). При первом входе принудительная смена.
8. **Лимит сотрудников.** На MVP — без жёсткого лимита (TBD).

### Матрица доступов (MVP)

| Функция | Owner | Admin CRM | Manager | Teacher |
|---------|-------|-----------|---------|---------|
| Создать сотрудника | ✅ | ✅ | ❌ | ❌ |
| Редактировать любого | ✅ | ✅ (кроме Owner) | ❌ | ❌ |
| Редактировать свой профиль | ✅ | ✅ | ✅ | ✅ (только bio/фото) |
| Сменить роль | ✅ | ❌ | ❌ | ❌ |
| Заблокировать | ✅ | ✅ (кроме Owner) | ❌ | ❌ |
| Удалить | ✅ | ❌ | ❌ | ❌ |
| Просмотр списка | ✅ | ✅ | ✅ | ❌ |

---

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

### SellerStaff (сотрудник организации)

| Атрибут | Тип | Описание |
|---------|-----|---------|
| staff_id | UUID | PK |
| seller_id | UUID FK | → Seller (организация) |
| account_id | UUID FK | → Account (SELLER_STAFF) |
| staff_role | StaffRole | owner / admin_crm / manager / teacher |
| staff_status | StaffStatus | active / blocked / archived |
| created_at | DateTime | |
| updated_at | DateTime | |

### PerformerProfile (публичный профиль преподавателя)

| Атрибут | Тип | Описание |
|---------|-----|---------|
| performer_id | UUID | PK |
| staff_id | UUID FK | → SellerStaff, unique |
| specialization | string? | до 100 символов |
| experience_years | int? | 0–70 |
| bio | text? | до 1000 символов |
| photo_url | string? | URL в CDN |
| is_active | boolean | default: true (false при блокировке) |
| updated_at | DateTime | |

### Account (расширение для SELLER_STAFF)

Дополнительное поле к существующей модели:

| Атрибут | Тип | Описание |
|---------|-----|---------|
| is_temp_password | boolean | default: true при создании через invite |

---

## 6. Технические контракты

### 6.1 Prisma Schema (добавления)

```prisma
enum StaffRole {
  owner
  admin_crm
  manager
  teacher
}

enum StaffStatus {
  active
  blocked
  archived
}

model SellerStaff {
  staff_id    String      @id @default(uuid())
  seller_id   String
  account_id  String      @unique
  staff_role  StaffRole
  staff_status StaffStatus @default(active)
  created_at  DateTime    @default(now())
  updated_at  DateTime    @updatedAt

  seller           Seller             @relation(fields: [seller_id], references: [seller_id])
  account          Account            @relation(fields: [account_id], references: [account_id])
  performer_profile PerformerProfile?
  item_performers  ItemPerformerLink[]
}

model PerformerProfile {
  performer_id      String   @id @default(uuid())
  staff_id          String   @unique
  specialization    String?  @db.VarChar(100)
  experience_years  Int?
  bio               String?  @db.Text
  photo_url         String?
  is_active         Boolean  @default(true)
  updated_at        DateTime @updatedAt

  staff SellerStaff @relation(fields: [staff_id], references: [staff_id])
}

// Добавление поля в Account:
model Account {
  // ... существующие поля ...
  is_temp_password Boolean @default(false)
}
```

### 6.2 TypeScript DTO

```typescript
// ─── Создание сотрудника ──────────────────────────────────────────────────

export class CreateStaffDto {
  @IsString() @MinLength(2) @MaxLength(50)
  first_name: string

  @IsString() @MinLength(2) @MaxLength(50)
  last_name: string

  @Matches(/^\+998\d{9}$/)
  phone: string

  @IsEmail()
  email: string

  @IsEnum(StaffRole)
  staff_role: StaffRole  // admin_crm | teacher (owner нельзя создать вручную)

  // Только для teacher:
  @IsOptional() @IsString() @MaxLength(100)
  specialization?: string

  @IsOptional() @IsInt() @Min(0) @Max(70)
  experience_years?: number

  @IsOptional() @IsString() @MaxLength(1000)
  bio?: string

  @IsOptional() @IsUrl()
  photo_url?: string
}

// ─── Редактирование ───────────────────────────────────────────────────────

export class UpdateStaffDto {
  @IsOptional() @IsString() @MinLength(2) @MaxLength(50)
  first_name?: string

  @IsOptional() @IsString() @MinLength(2) @MaxLength(50)
  last_name?: string

  @IsOptional() @Matches(/^\+998\d{9}$/)
  phone?: string

  @IsOptional() @IsEmail()
  email?: string

  @IsOptional() @IsEnum(StaffRole)
  staff_role?: StaffRole  // только Owner может менять
}

export class UpdatePerformerProfileDto {
  @IsOptional() @IsString() @MaxLength(100)
  specialization?: string

  @IsOptional() @IsInt() @Min(0) @Max(70)
  experience_years?: number

  @IsOptional() @IsString() @MaxLength(1000)
  bio?: string

  @IsOptional() @IsUrl()
  photo_url?: string
}

// ─── Response ─────────────────────────────────────────────────────────────

export interface StaffListItemResponse {
  staff_id: string
  first_name: string
  last_name: string
  phone: string
  email: string
  staff_role: StaffRole
  staff_status: StaffStatus
  performer_profile: PerformerProfileDto | null  // только для teacher
}

export interface PerformerProfileDto {
  performer_id: string
  specialization: string | null
  experience_years: number | null
  bio: string | null
  photo_url: string | null
  is_active: boolean
}

// Public response (для публичных страниц — без контактных данных)
export interface PerformerPublicDto {
  performer_id: string
  first_name: string
  last_name: string
  specialization: string | null
  experience_years: number | null
  bio: string | null
  photo_url: string | null
}
```

### 6.3 API Endpoints

```
────────────────────────────────────────────────────────────────
SELLER: УПРАВЛЕНИЕ СОТРУДНИКАМИ
────────────────────────────────────────────────────────────────

GET /api/seller/staff
Auth: Bearer (seller)
Query: ?role=teacher&status=active&page=1&limit=50
→ 200: { staff: StaffListItemResponse[], total: number }
→ 403: { error: 'INSUFFICIENT_ROLE' }  // Teacher не видит список

POST /api/seller/staff
Auth: Bearer (seller: owner | admin_crm)
Body: CreateStaffDto
→ 201: StaffListItemResponse
→ 409: { error: 'EMAIL_TAKEN' | 'PHONE_TAKEN', message: string }
→ 400: { error: 'CANNOT_CREATE_OWNER', message: 'Роль Owner нельзя создать вручную.' }
→ 422: { errors: ValidationError[] }

GET /api/seller/staff/:staff_id
Auth: Bearer (seller)
→ 200: StaffListItemResponse
→ 403: { error: 'FORBIDDEN' }  // нельзя смотреть чужих сотрудников
→ 404: { error: 'STAFF_NOT_FOUND' }

PATCH /api/seller/staff/:staff_id
Auth: Bearer (seller: owner | admin_crm | teacher[self only])
Body: UpdateStaffDto
→ 200: StaffListItemResponse
→ 400: { error: 'CANNOT_DEMOTE_LAST_OWNER', message: 'Нельзя сменить роль последнего владельца.' }
→ 403: { error: 'CANNOT_EDIT_OWNER' }  // admin пытается редактировать owner
→ 422: { errors: ValidationError[] }

PATCH /api/seller/staff/:staff_id/performer-profile
Auth: Bearer (seller: owner | admin_crm | teacher[self only])
Body: UpdatePerformerProfileDto
→ 200: PerformerProfileDto
→ 400: { error: 'NOT_TEACHER', message: 'Профиль преподавателя доступен только для роли Teacher.' }

PATCH /api/seller/staff/:staff_id/status
Auth: Bearer (seller: owner | admin_crm)
Body: { status: 'active' | 'blocked' }
→ 200: { staff_id: string, staff_status: StaffStatus }
→ 400: { error: 'CANNOT_BLOCK_LAST_OWNER', message: 'Нельзя заблокировать последнего владельца.' }
→ 403: { error: 'CANNOT_BLOCK_OWNER' }  // admin пытается заблокировать owner

DELETE /api/seller/staff/:staff_id
Auth: Bearer (seller: owner only)
→ 204
→ 400: { error: 'CANNOT_DELETE_LAST_OWNER', message: 'Нельзя удалить последнего владельца.' }
→ 403: { error: 'INSUFFICIENT_ROLE', message: 'Только владелец может удалять сотрудников.' }

POST /api/seller/staff/:staff_id/resend-invite
Auth: Bearer (seller: owner | admin_crm)
→ 200: { message: 'Приглашение отправлено на {email}.' }
→ 400: { error: 'ALREADY_ACTIVATED', message: 'Сотрудник уже использовал приглашение.' }

────────────────────────────────────────────────────────────────
ПУБЛИЧНЫЙ API: ПРОФИЛИ ПРЕПОДАВАТЕЛЕЙ
────────────────────────────────────────────────────────────────

GET /api/sellers/:seller_id/performers
Auth: Public
→ 200: PerformerPublicDto[]  // только active performers

GET /api/items/:item_id/performers
Auth: Public
→ 200: PerformerPublicDto[]  // преподаватели конкретного курса

────────────────────────────────────────────────────────────────
AUTH: СМЕНА ВРЕМЕННОГО ПАРОЛЯ
────────────────────────────────────────────────────────────────

POST /api/auth/change-temp-password
Auth: Bearer (staff с is_temp_password = true)
Body: { new_password: string, confirm_password: string }
→ 200: { success: true }
→ 400: { error: 'PASSWORDS_DO_NOT_MATCH' | 'PASSWORD_TOO_WEAK', message: string }
```

---

## 7. Edge Cases и обработка ошибок

| Сценарий | Поведение |
|----------|----------|
| Owner пытается удалить себя | 400 CANNOT_DELETE_LAST_OWNER |
| Admin CRM пытается изменить роль | 403 INSUFFICIENT_ROLE (только Owner меняет роли) |
| Удаление преподавателя привязанного к айтемам | ItemPerformerLink каскадно удаляются, предупреждение в UI |
| Сотрудник входит со старым паролем после смены | 401 INVALID_CREDENTIALS, стандартная ошибка |
| Resend invite для уже активного сотрудника | 400 ALREADY_ACTIVATED |
| Teacher пытается просмотреть список сотрудников | 403 INSUFFICIENT_ROLE |
| Seller заблокирован (account_status = blocked) — его преподаватели | PerformerProfile возвращается в публичных API только если seller.account_status = active |
| Создание сотрудника с email который используется как Buyer | 409 EMAIL_TAKEN — нет multi-role для staff (только buyer↔seller, не buyer↔staff) |
| Параллельное добавление двух сотрудников с одним email | Unique constraint в Account.email поймает второй запрос |

---

## 8. TBD / Сознательно опущено

| Тема | Статус | Примечание |
|------|--------|-----------|
| Вход Teacher в кабинет (MVP) | Частично | Teacher может войти и видит только /seller/me (свой профиль). Полноценный Teacher-dashboard — v1.0 |
| Роли Manager в MVP | Частично | Manager создаётся, но его дополнительные права (CRM) активны только в v1.0 |
| Передача роли Owner | Исключено из MVP | Только через поддержку. Интерфейс — v1.0 |
| Лимит сотрудников | TBD | Нет лимита на MVP. Нужен ли лимит по тарифу — TBD |
| SMTP провайдер (email-приглашения) | TBD | Провайдер не определён |
| SMS-приглашение (вместо email) | Исключено из MVP | Только email |
| Удаление аккаунта при удалении сотрудника | TBD | Soft delete staff_status = archived. Нужно ли также блокировать Account? Да — добавлено в UC-05 |
| Аудит лог (кто создал/изменил сотрудника) | Исключено из MVP | EventLog/SAA |
| Фото преподавателя: thumbnail генерация | TBD | Аналогично Spec 02 |
| Temporary password expiry | TBD | Нужно ли время жизни для временного пароля? (например 7 дней) |

---

## Зависимости

| Модуль | Связь |
|--------|-------|
| **Spec 01** (Seller Profile) | SellerStaff.seller_id → Seller.seller_id |
| **Spec 02** (Items) | ItemPerformerLink.seller_staff_id → SellerStaff.staff_id |
| **Spec 04** (Admin) | Публичный PerformerProfile виден только при active seller |
| **Spec 12** (Public Profile) | GET /api/sellers/:id/performers используется на публичной странице школы |
| **Spec v1.0** (CRM Roles) | Полная матрица доступов для Manager и Teacher реализуется в v1.0 |
