wasrusgen1-crm/docs/SCHEMA.md

529 lines
41 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# SCHEMA.md — Контракт данных CRM мебельного салона
**Проект:** CRM для сети мебельных салонов (РФ)
**Версия:** 1.1 · 2026-05-29
**Реализация:** JavaScript, in-memory в `data.js` (в перспективе — `localStorage`). Бэкенда в первом прототипе нет.
**Соглашения:** поля — `camelCase` (английские); id — строки вида `'ord_001'`, `'cli_001'`, `'usr_001'`.
Этот документ — **единственный источник истины** по структуре данных. Если экран показывает поле, которого здесь нет, — прав документ, а не экран. Если разработчику что-то непонятно — это баг документа, заведите вопрос, не додумывайте.
---
## 0. Что этот контракт чинит (из аудита)
| Проблема аудита | Решение в схеме |
|---|---|
| Заказы и клиенты не существуют в общем источнике | Сущности `orders`, `clients` в `data.js` |
| Клиент — строка, не сущность | `clients[]` с `id`, заказы ссылаются `clientId` |
| KPI захардкожены | KPI вынесены в раздел 5 «Вычисляемые поля», не хранятся |
| Связи по именам | Все связи через `id` (раздел 2) |
| 3 формата времени | Единый стандарт: ISO 8601 (раздел 6, БП-1) |
| Enum как свободные строки | Все enum зафиксированы в `_DICT` (раздел 3) |
| Комиссии и конфиги в вёрстке | Сущность `config` + `commissionRules` (раздел 1.7) |
| id менеджера по инициалам (`'ak'`) | `id` = `'usr_001'` (стабильный), инициалы → поле `initials` |
| Оценки сотрудников нигде не хранятся / непрозрачны | Сущность `ratings` (раздел 1.8), рейтинг вычисляется (раздел 5), анонимность для оцениваемого (БП-14) |
| Ставка комиссии захардкожена одним числом | Трёхуровневый приоритет: `orders.commissionRate``users.commissionRate``config.commissionRules` (БП-5) |
---
## 1. Сущности (хранимые)
Порядок объявления в `data.js`: `config``salons``users``clients``orders``appointments``requests``shiftRequests``ratings`. Сущность не может ссылаться на ещё не объявленную — отсюда порядок.
### 1.1 `salons` — салоны сети
| Поле | Тип | Обяз. | Пример | Комментарий |
|---|---|:---:|---|---|
| `id` | string | да | `'sal_lenina'` | Первичный ключ. Префикс `sal_`. Неизменяемый. |
| `name` | string | да | `'Салон на Ленина'` | Отображаемое название. |
| `address` | string | да | `'пр. Ленина, 42'` | Физический адрес. |
| `color` | string | да | `'#3B82F6'` | HEX-цвет для UI (графики, бейджи салона). |
| `adminId` | string\|null | да | `'usr_002'` | FK → `users.id`. Директор салона. `null` если вакансия. |
| `revenuePlan` | number | да | `1700000` | План выручки на текущий месяц, ₽. Целое. |
| `ordersPlan` | number | да | `30` | План по кол-ву заказов на месяц. Целое. |
| `active` | boolean | да | `true` | `false` — салон закрыт/заморожен, скрыт из активных списков. |
> **Не хранить здесь:** `orders`, `revenue`, `overdue`, `newLeads` — это факт, вычисляется из `orders` (раздел 5).
### 1.2 `users` — сотрудники (все роли)
| Поле | Тип | Обяз. | Пример | Комментарий |
|---|---|:---:|---|---|
| `id` | string | да | `'usr_001'` | Первичный ключ. Префикс `usr_` + порядковый номер. **Никогда не основан на инициалах** (коллизии). Неизменяемый. |
| `name` | string | да | `'Анна Кузнецова'` | Полное имя. |
| `initials` | string | да | `'АК'` | Инициалы для аватара/компактного UI. Только для отображения, **не ключ**. |
| `role` | string | да | `'manager'` | Enum → `_DICT.role`. См. 3.1. |
| `salonId` | string\|null | да | `'sal_lenina'` | FK → `salons.id`. У КД — `null` (над салонами). |
| `color` | string | да | `'#7C3AED'` | HEX-цвет сотрудника для шахматки/графиков. |
| `phone` | string | нет | `'+79110000000'` | Контакт. |
| `active` | boolean | да | `true` | `false` — уволен/в отпуске; скрыт из распределения заказов, история сохраняется. |
| `commissionRate` | number\|null | нет | `0.05` | Индивидуальная ставка комиссии (доля, 0.05 = 5%). `null` → берётся из `config.commissionRules` по роли. |
> **Не хранить здесь:** `visits`, `deals`, `revenue`, `conversion`, `avg`, `rating`, `kpi` — всё это KPI, вычисляется (раздел 5). Хранение = тот самый «захардкоженный KPI» из аудита.
### 1.3 `clients` — клиенты (сущность, не строка)
| Поле | Тип | Обяз. | Пример | Комментарий |
|---|---|:---:|---|---|
| `id` | string | да | `'cli_001'` | Первичный ключ. Префикс `cli_`. Неизменяемый. |
| `name` | string | да | `'Орлова Мария'` | ФИО клиента. |
| `phone` | string | да | `'+79219998877'` | Основной контакт. Уникален в пределах сети (БП-6). |
| `email` | string\|null | нет | `'orlova@mail.ru'` | Необязателен. |
| `source` | string | да | `'instagram'` | Откуда пришёл. Enum → `_DICT.clientSource`. См. 3.4. |
| `salonId` | string | да | `'sal_lenina'` | FK → `salons.id`. Салон, к которому привязан клиент. |
| `managerId` | string\|null | да | `'usr_001'` | FK → `users.id`. Ответственный менеджер. `null` — не распределён. |
| `status` | string | да | `'lead'` | Enum → `_DICT.clientStatus`. См. 3.5. |
| `createdAt` | string (ISO) | да | `'2026-05-20T09:14:00+03:00'` | Дата появления в CRM. ISO 8601. |
| `note` | string\|null | нет | `'Интересует кухня Hettich'` | Свободный комментарий. |
> **Не хранить здесь:** число заказов, сумму покупок, LTV — вычисляется из `orders` по `clientId` (раздел 5).
### 1.4 `orders` — заказы (новая сущность, центральная)
| Поле | Тип | Обяз. | Пример | Комментарий |
|---|---|:---:|---|---|
| `id` | string | да | `'ord_001'` | Первичный ключ. Префикс `ord_`. Неизменяемый. |
| `clientId` | string | да | `'cli_001'` | FK → `clients.id`. Обязательна (БП-2). |
| `managerId` | string | да | `'usr_001'` | FK → `users.id`. Менеджер, оформивший заказ. |
| `salonId` | string | да | `'sal_lenina'` | FK → `salons.id`. Денормализация ради быстрых выборок по салону; должна совпадать с `clients.salonId`. |
| `status` | string | да | `'measuring'` | Enum → `_DICT.orderStatus`. См. 3.2. |
| `amount` | number | да | `60500` | Сумма заказа, ₽. Целое. |
| `prepayment` | number | да | `20000` | Внесённая предоплата, ₽. `0` если нет. |
| `commissionRate` | number\|null | нет | `0.10` | Переопределение ставки комиссии для этого заказа. `null` → используется ставка менеджера (`users.commissionRate`) или дефолт из `config` (БП-5). |
| `createdAt` | string (ISO) | да | `'2026-05-20T10:30:00+03:00'` | Когда оформлен. ISO 8601. |
| `dueDate` | string (ISO date)\|null | да | `'2026-06-15'` | Плановая дата готовности/выдачи. `null` — не назначена. Формат `YYYY-MM-DD`. |
| `closedAt` | string (ISO)\|null | да | `null` | Когда закрыт (`done`/`canceled`). `null` пока открыт. |
| `paymentStatus` | string | да | `'partial'` | Enum → `_DICT.paymentStatus`. См. 3.3. |
> **Не хранить:** «просрочен ли заказ», «сумма риска» — вычисляется из `dueDate` + `status` + текущей даты (раздел 5, `isOverdue`).
### 1.5 `appointments` — записи в расписании (шахматка)
Заменяет `_CHESS_DATA`. Вместо матрицы `менеджер × строка-времени` — плоский список событий с нормальным временем.
| Поле | Тип | Обяз. | Пример | Комментарий |
|---|---|:---:|---|---|
| `id` | string | да | `'apt_001'` | Первичный ключ. Префикс `apt_`. |
| `managerId` | string | да | `'usr_001'` | FK → `users.id`. Кто проводит. |
| `clientId` | string\|null | да | `'cli_001'` | FK → `clients.id`. `null` — слот занят без клиента (бронь/перерыв). |
| `salonId` | string | да | `'sal_lenina'` | FK → `salons.id`. |
| `type` | string | да | `'consult'` | Enum → `_DICT.appointmentType`. См. 3.6. |
| `status` | string | да | `'busy'` | Enum → `_DICT.appointmentStatus`. См. 3.7. |
| `startAt` | string (ISO) | да | `'2026-05-29T14:00:00+03:00'` | Начало. ISO 8601 с таймзоной. |
| `endAt` | string (ISO) | да | `'2026-05-29T16:00:00+03:00'` | Конец. Длительность = `endAt startAt` (БП-3). |
| `orderId` | string\|null | нет | `'ord_001'` | FK → `orders.id`, если визит привязан к заказу. |
> Слоты «свободно» в UI **не хранятся** — это пустоты между `appointments` на рабочей сетке (раздел 5, `freeSlots`).
### 1.6 `requests` — заявки менеджеров администратору / КД
Заменяет `_MGR_REQUESTS`.
| Поле | Тип | Обяз. | Пример | Комментарий |
|---|---|:---:|---|---|
| `id` | string | да | `'req_001'` | Первичный ключ. Префикс `req_`. |
| `authorId` | string | да | `'usr_001'` | FK → `users.id`. Кто создал. |
| `salonId` | string | да | `'sal_lenina'` | FK → `salons.id`. |
| `type` | string | да | `'supply'` | Enum → `_DICT.requestType`. См. 3.8. |
| `priority` | string | да | `'high'` | Enum → `_DICT.priority`. См. 3.9. |
| `title` | string | да | `'Закончились образцы ткани'` | Короткий заголовок. |
| `body` | string | да | `'Нет образцов искусственной замши...'` | Текст заявки. |
| `status` | string | да | `'new'` | Enum → `_DICT.requestStatus`. См. 3.10. |
| `createdAt` | string (ISO) | да | `'2026-05-29T09:14:00+03:00'` | ISO 8601. Заменяет `'сегодня 09:14'`. |
| `assigneeId` | string\|null | нет | `'usr_002'` | FK → `users.id`. Кому эскалировано (`null` — на администратора салона по умолчанию). |
### 1.7 `config` — конфигурация и комиссии (singleton)
Заменяет конфиги и комиссии, зашитые в вёрстку. Один объект на всю систему.
| Поле | Тип | Обяз. | Пример | Комментарий |
|---|---|:---:|---|---|
| `kpiNorm` | number | да | `80` | Норма KPI (порог), %. Ниже — флаг. Было захардкожено в мокапе. |
| `currency` | string | да | `'RUB'` | Валюта системы. |
| `timezone` | string | да | `'Europe/Moscow'` | Базовая таймзона для расчётов дат. |
| `workdayStart` | string | да | `'10:00'` | Начало рабочего дня (для сетки шахматки). |
| `workdayEnd` | string | да | `'20:00'` | Конец рабочего дня. |
| `slotMinutes` | number | да | `60` | Шаг сетки расписания, минут. |
| `commissionRules` | array | да | см. ниже | Дефолтные ставки комиссии по ролям. Ставка по умолчанию — 7% (`0.07`). |
`commissionRules[]` — элемент:
| Поле | Тип | Пример | Комментарий |
|---|---|---|---|
| `role` | string | `'manager'` | Enum → `_DICT.role`. |
| `rate` | number | `0.07` | Доля от `amount` закрытого заказа (0.07 = 7%). |
> Приоритет ставки (от высшего к низшему): `orders.commissionRate` (ставка конкретного заказа) → `users.commissionRate` (индивидуальная ставка менеджера) → `config.commissionRules` по `role` (дефолт, 7% для менеджера). Комиссия начисляется только с заказов в статусе `done` (БП-5).
### 1.8 `ratings` — оценки этапов заказа и сотрудников
Хранит оценки, которые участники выставляют друг другу и заказу. Рейтинг сотрудника не хранится — вычисляется (раздел 5).
| Поле | Тип | Обяз. | Пример | Комментарий |
|---|---|:---:|---|---|
| `id` | string | да | `'rat_001'` | Первичный ключ. Префикс `rat_`. |
| `orderId` | string | да | `'ord_001'` | FK → `orders.id`. |
| `stage` | string | да | `'assembly'` | Этап заказа, для которого оценка. Enum → `_DICT.ratingStage`. См. 3.13. |
| `authorId` | string | да | `'usr_001'` | FK → `users.id`. Кто оставил оценку. **Скрыт от оцениваемого** (БП-14). |
| `targetId` | string | да | `'usr_003'` | FK → `users.id`. Кого оценивают. |
| `scores` | object | да | `{quality:8, speed:7, cleanliness:9}` | Набор критериев → оценка 110 (целое). Ключи зависят от `stage`. См. 3.14. |
| `comment` | string\|null | нет | `'Аккуратная работа'` | Свободный комментарий. |
| `createdAt` | string (ISO) | да | `'2026-06-01T15:00:00+03:00'` | Когда оставлена. ISO 8601. |
**Кто кого оценивает:**
- Клиент оценивает весь заказ (обязательно при закрытии — БП-13). `stage='order'`.
- Менеджер оценивает замерщика (`stage='measuring'`) и сборщика (`stage='assembly'`).
- Замерщик оценивает менеджера.
- Сборщик оценивает менеджера.
- Администратор и КД видят все оценки, включая `authorId` (кто оценил).
- Оцениваемый (`targetId`) видит только итоговые оценки — **не видит, кто конкретно его оценил** (БП-14).
**Шкала:** 110 (целое). Среднее арифметическое по критериям `scores` = итоговая оценка за этап. Рейтинг сотрудника = среднее по всем его этапам за период (раздел 5).
---
## 2. Связи (явные)
Все связи — по `id`, не по имени. `N→1` читается «много слева на одного справа».
```
salons.adminId → users.id (1→1, директор салона)
users.salonId → salons.id (N→1, сотрудник в салоне; null у КД)
clients.salonId → salons.id (N→1)
clients.managerId → users.id (N→1, ответственный менеджер; null = не распределён)
orders.clientId → clients.id (N→1, у заказа всегда есть клиент)
orders.managerId → users.id (N→1)
orders.salonId → salons.id (N→1, денормализация = clients.salonId)
appointments.managerId → users.id (N→1)
appointments.clientId → clients.id (N→1; null допустим)
appointments.salonId → salons.id (N→1)
appointments.orderId → orders.id (N→1; nullable)
requests.authorId → users.id (N→1)
requests.salonId → salons.id (N→1)
requests.assigneeId → users.id (N→1; nullable)
shiftRequests.authorId → users.id (N→1)
shiftRequests.withUserId→ users.id (N→1; null для отгула)
ratings.orderId → orders.id (N→1)
ratings.authorId → users.id (N→1, кто оценил; скрыт от targetId)
ratings.targetId → users.id (N→1, кого оценивают)
commissionRules.role → _DICT.role (значение из справочника)
```
Диаграмма верхнего уровня:
```
┌──────────┐
│ salons │◄──────────────┐
└────┬─────┘ │ salonId (денормализация)
adminId │ salonId │
▼ │
┌──────────┐ managerId │
┌───────►│ users │◄──────────────┤
│ └────┬─────┘ │
│ authorId │ managerId │
│ ▼ │
│ ┌──────────┐ clientId ┌──┴──────┐
│ │ clients │◄───────────┤ orders │
│ └──────────┘ └─────────┘
│ ▲ ▲
│ │ clientId │ orderId
│ ┌────┴──────────┐ │
│ │ appointments ├────────────┘
│ └───────────────┘
┌──┴──────────┐ ┌───────────────┐ ┌──────────┐
│ requests │ │ shiftRequests │ │ ratings │
└─────────────┘ └───────────────┘ └──────────┘
```
---
## 3. Справочники (`_DICT`) — все enum зафиксированы
Хранятся в `data.js` как `var _DICT = { ... }`. **В полях сущностей допустимы только ключи из этих справочников.** Свободные строки запрещены (правка аудита). UI берёт `label`/`color` отсюда, а не хардкодит.
### 3.1 `role` — роль сотрудника
| Ключ | Русское название | Описание |
|---|---|---|
| `owner` | КД (собственник) | Видит всю сеть. `salonId = null`. |
| `admin` | Администратор | Директор одного салона. |
| `manager` | Менеджер | Продавец-консультант. |
| `measurer` | Замерщик | Проводит замеры, привязан к салону. |
| `assembler` | Сборщик | Выполняет монтаж, привязан к салону. |
> Роли `measurer` и `assembler` активны в v1 — реализованы в мокапах `mockup_measurer.html` и `mockup_assembler.html`.
### 3.2 `orderStatus` — статус заказа
| Ключ | Русское название | Описание |
|---|---|---|
| `lead` | Лид | Новый контакт, не конвертирован в заказ. |
| `measuring` | Замер | Назначен/проводится замер. |
| `project` | Проект | Разрабатывается дизайн-проект. |
| `tech` | Техника | Подбор и согласование техники. |
| `technolog` | Технолог | Проверка технологом. |
| `production` | Производство | Передан на фабрику. |
| `assembly` | Сборка | Монтаж у клиента. |
| `done` | Закрыт | Выдан, оплачен. Начисляется комиссия. |
| `canceled` | Отменён | Сделка не состоялась. |
### 3.3 `paymentStatus` — статус оплаты
| Ключ | Русское название | Описание |
|---|---|---|
| `none` | Без оплаты | Предоплаты нет. |
| `partial` | Частичная | Внесена предоплата, остаток не закрыт. |
| `paid` | Оплачен | Оплачено полностью. |
| `refunded` | Возврат | Средства возвращены клиенту. |
### 3.4 `clientSource` — источник клиента
| Ключ | Русское название | Описание |
|---|---|---|
| `walkin` | Зашёл в салон | Офлайн-трафик. |
| `instagram` | Instagram | Соцсеть. |
| `avito` | Avito | Доска объявлений. |
| `referral` | Рекомендация | По совету клиента. |
| `site` | Сайт | Заявка с сайта. |
| `other` | Другое | Прочее. |
### 3.5 `clientStatus` — статус клиента в воронке
| Ключ | Русское название | Описание |
|---|---|---|
| `lead` | Лид | Новый контакт, без заказа. |
| `active` | Активный | Есть открытый заказ. |
| `repeat` | Повторный | Был ≥1 закрытый заказ. |
| `lost` | Потерян | Отказ/неактивен. |
### 3.6 `appointmentType` — тип записи в расписании
| Ключ | Русское название | Описание |
|---|---|---|
| `consult` | Консультация | Первичная встреча/подбор. |
| `follow` | Повторный контакт | Дозвон/повторная встреча. |
| `measure` | Замер | Выезд/замер. |
| `tech` | Техническая | Согласование проекта, документы. |
### 3.7 `appointmentStatus` — статус записи
| Ключ | Русское название | Описание |
|---|---|---|
| `busy` | Занято | Слот забронирован, событие предстоит. |
| `done` | Завершено | Встреча прошла. |
| `noshow` | Не пришёл | Клиент не явился. |
| `canceled` | Отменено | Запись отменена. |
> Статуса `free` нет: свободное время — это отсутствие записи, а не запись со статусом (см. раздел 5, `freeSlots`).
### 3.8 `requestType` — тип заявки
| Ключ | Русское название | Описание |
|---|---|---|
| `supply` | Снабжение | Образцы, каталоги, материалы. |
| `escalate` | Эскалация | Конфликт/проблема, нужно вмешательство. |
| `visit` | Выезд/транспорт | Нужен ресурс для выезда к клиенту. |
| `schedule` | Расписание | Вопрос по сменам/графику. |
| `other` | Другое | Прочее. |
### 3.9 `priority` — приоритет
| Ключ | Русское название | Описание |
|---|---|---|
| `low` | Низкий | Можно отложить. |
| `normal` | Обычный | Штатный. |
| `high` | Высокий | Срочно, влияет на деньги/клиента. |
### 3.10 `requestStatus` — статус заявки
| Ключ | Русское название | Описание |
|---|---|---|
| `new` | Новая | Не обработана. |
| `inWork` | В работе | Взята в обработку. |
| `done` | Выполнена | Закрыта. |
| `rejected` | Отклонена | Отказано. |
### 3.11 `shiftRequestType` — тип запроса по смене
| Ключ | Русское название | Описание |
|---|---|---|
| `swap` | Обмен сменами | С другим сотрудником (`withUserId` обязателен). |
| `off` | Отгул | Без замены (`withUserId = null`). |
| `extra` | Доп. смена | Просьба добавить смену. |
### 3.12 `shiftRequestStatus` — статус запроса по смене
| Ключ | Русское название | Описание |
|---|---|---|
| `pending` | На рассмотрении | Ждёт решения администратора. |
| `approved` | Одобрено | Согласовано. |
| `declined` | Отклонено | Отказано. |
### 3.13 `ratingStage` — этап заказа, для которого выставлена оценка
| Ключ | Название |
|---|---|
| `measuring` | Замер |
| `project` | Проект |
| `assembly` | Сборка |
| `order` | Весь заказ (клиентская оценка) |
### 3.14 `ratingCriteria` — критерии оценки (ключи `scores`)
| Ключ | Название | Этапы |
|---|---|---|
| `quality` | Качество работы | measuring, project, assembly |
| `speed` | Скорость | measuring, assembly |
| `cleanliness` | Чистота | assembly |
| `deadlines` | Сроки | project, assembly |
| `communication` | Взаимодействие | measuring |
| `overall` | Общая оценка | order |
| `service` | Сервис | order |
| `result` | Результат | order |
> Набор критериев в `ratings.scores` зависит от `stage`: Замер → `quality`, `speed`, `communication`; Проект → `quality`, `deadlines`; Сборка → `quality`, `speed`, `cleanliness`, `deadlines`; Весь заказ → `overall`, `service`, `result`.
---
## 4. Сущность `shiftRequests` — запросы по сменам
Заменяет `_SHIFT_REQS`. Вынесена отдельно от справочников для полноты.
| Поле | Тип | Обяз. | Пример | Комментарий |
|---|---|:---:|---|---|
| `id` | string | да | `'shr_001'` | Префикс `shr_`. |
| `authorId` | string | да | `'usr_002'` | FK → `users.id`. Кто просит. |
| `salonId` | string | да | `'sal_lenina'` | FK → `salons.id`. |
| `type` | string | да | `'swap'` | Enum → `_DICT.shiftRequestType`. См. 3.11. |
| `withUserId` | string\|null | да | `'usr_003'` | FK → `users.id`. С кем обмен; `null` для `off`. |
| `dateFrom` | string (ISO date) | да | `'2026-05-31'` | Дата запроса. `YYYY-MM-DD`. Заменяет `'31 мая ↔ 1 июня'`. |
| `dateTo` | string (ISO date)\|null | нет | `'2026-06-01'` | Вторая дата для обмена; `null` если одна. |
| `status` | string | да | `'pending'` | Enum → `_DICT.shiftRequestStatus`. См. 3.12. |
---
## 5. Вычисляемые поля (НЕ хранятся)
Считаются на лету из хранимых данных. **Запрещено сохранять как поля сущностей** — это и есть «захардкоженный KPI» из аудита. Реализуются хелперами в `data.js`.
### По менеджеру (`users` с `role='manager'`), за период
| Поле | Формула (словами) | Источник |
|---|---|---|
| `visits` | Кол-во `appointments` менеджера со `status='done'` за период | `appointments` |
| `deals` | Кол-во `orders` менеджера со `status='done'` за период | `orders` |
| `revenue` | Сумма `amount` закрытых заказов менеджера за период | `orders` |
| `conversion` | `deals / visits × 100`, % (0 если `visits=0`) | производное |
| `avg` | `revenue / deals`, средний чек (0 если `deals=0`) | производное |
| `commission` | По БП-5: ставка × сумма закрытых заказов | `orders` + `users` + `config` |
| `kpi` | Сводный балл; базово = `conversion`, сравнивается с `config.kpiNorm` | производное |
| `belowNorm` | `kpi < config.kpiNorm` → флаг | производное |
### По сотруднику — рейтинг из оценок
| Поле | Формула (словами) | Источник |
|---|---|---|
| `employeeRating(userId, period)` | Среднее всех значений `scores` по всем `ratings` где `targetId=userId` за период | `ratings` |
| `orderRating(orderId)` | Клиентская оценка заказа: из `ratings` с `orderId`, `stage='order'` и `authorId = clientId` заказа | `ratings` |
> Рейтинг сотрудника = среднее всех `scores` по всем его этапам за период (БП-15). Не хранится.
### По салону (`salons`)
| Поле | Формула | Источник |
|---|---|---|
| `orders` | Кол-во заказов салона за месяц | `orders` по `salonId` |
| `revenue` | Сумма `amount` закрытых заказов салона за месяц | `orders` |
| `overdue` | Кол-во заказов с `isOverdue=true` | `orders` |
| `overdueRisk` | Сумма `amount` просроченных заказов | `orders` |
| `newLeads` | Кол-во `clients` со `status='lead'` за период | `clients` |
| `planFulfillment` | `revenue / revenuePlan × 100`, % | производное |
| `salonStatus` | `'ok'`/`'warn'`/`'risk'` по `planFulfillment` и `overdue` | производное |
### По заказу (`orders`)
| Поле | Формула | Источник |
|---|---|---|
| `isOverdue` | `dueDate < сегодня` И `status ∉ {done, canceled}` | `orders` + дата |
| `balance` | `amount prepayment`, остаток к оплате | производное |
### По клиенту (`clients`)
| Поле | Формула | Источник |
|---|---|---|
| `ordersCount` | Кол-во заказов клиента | `orders` по `clientId` |
| `ltv` | Сумма `amount` закрытых заказов клиента | `orders` |
### По расписанию
| Поле | Формула | Источник |
|---|---|---|
| `freeSlots` | Слоты рабочей сетки (`config.workdayStart..End` шагом `slotMinutes`) без `appointments` | производное |
---
## 6. Бизнес-правила (утверждения, не код)
- **БП-1. Единый формат времени.** Все моменты времени хранятся как ISO 8601 с таймзоной (`'2026-05-29T14:00:00+03:00'`); только-дата — как `YYYY-MM-DD`. Человекочитаемые строки (`'сегодня 09:14'`, `'31 мая'`) и сетки-слоты как ключи запрещены. Форматирование для UI — на стороне отображения.
- **БП-2. Заказ без клиента не существует.** `orders.clientId` обязателен и должен указывать на существующего `clients.id`. Создать заказ можно только после создания клиента.
- **БП-3. Длительность записи = разница времён.** `appointments.endAt > startAt`. Длительность не хранится отдельным полем.
- **БП-4. Согласованность салона.** `orders.salonId` должен совпадать с `clients.salonId` ответственного клиента. При смене салона клиента — пересобрать денормализацию.
- **БП-5. Комиссия только с закрытых заказов.** Начисляется лишь для `orderStatus='done'`. Ставка определяется в порядке приоритета: `orders.commissionRate``users.commissionRate``config.commissionRules[role].rate` (дефолт 7%). База — `orders.amount`. Результат — `getCommission(userId, period)` в разделе 5.
- **БП-6. Телефон клиента уникален в сети.** Дубликат телефона = тот же клиент. Повторное обращение не создаёт нового `clients`, а привязывается к существующему.
- **БП-7. KPI не хранится.** Любой показатель эффективности (раздел 5) вычисляется. Поле KPI в хранимой сущности — ошибка ревью.
- **БП-8. Enum только из `_DICT`.** Запись значения вне справочника недопустима. Новое значение → сначала в `_DICT`, потом в данные.
- **БП-9. id неизменяем и осмыслен по префиксу.** После создания `id` не меняется (на него ссылаются). Префиксы: `sal_`, `usr_`, `cli_`, `ord_`, `apt_`, `req_`, `shr_`, `rat_`.
- **БП-10. Удаление — мягкое.** Сотрудники/салоны/клиенты не удаляются физически (на них висит история заказов) — ставится `active=false` / `status='lost'`. Жёсткое удаление запрещено.
- **БП-11. Эскалация = смена адресата.** `requests` по умолчанию идёт администратору салона. КД видит её, только если `assigneeId` указывает на пользователя с `role='owner'` (эскалация) или `type='escalate'`.
- **БП-12. КД вне салона.** У `role='owner'` `salonId = null`. Любая выборка «по салону» для КД означает «по всем салонам».
- **БП-13. Клиентская оценка обязательна при закрытии.** Перевод заказа в статус `done` невозможен без клиентской оценки (`ratings` с `stage='order'`). Без неё закрыть заказ нельзя.
- **БП-14. Анонимность оценки для оцениваемого.** Оцениваемый (`targetId`) не видит `authorId`. Только пользователи с ролью `admin` и `owner` видят, кто оценил.
- **БП-15. Рейтинг сотрудника вычисляется.** Рейтинг = среднее всех `scores` по всем `ratings` где `targetId=userId` за период. Не хранится — вычисляется (раздел 5, `employeeRating`).
---
## 7. Доступ по ролям
Принцип: **менеджер видит своё, администратор — свой салон, КД — всю сеть. Замерщик и сборщик — только свои назначенные этапы.**
| Сущность / данные | Менеджер (`manager`) | Замерщик (`measurer`) | Сборщик (`assembler`) | Администратор (`admin`) | КД (`owner`) |
|---|---|---|---|---|---|
| Свои клиенты | ✅ чтение/запись | ❌ | ❌ | ✅ все клиенты салона | ✅ все клиенты сети |
| Чужие клиенты | ❌ | ❌ | ❌ | ✅ в своём салоне | ✅ |
| Свои заказы | ✅ чтение/запись | ✅ только назначенные (этап замера) | ✅ только назначенные (этап сборки) | ✅ все заказы салона | ✅ все заказы сети |
| Чужие заказы | ❌ | ❌ | ❌ | ✅ свой салон | ✅ |
| Финансы заказа (`amount`, комиссия) | ✅ свои | ❌ | ❌ | ✅ свой салон | ✅ |
| Своё расписание (`appointments`) | ✅ чтение/запись | ✅ своё | ✅ своё | ✅ весь салон (шахматка) | ✅ все салоны |
| Свои KPI | ✅ только просмотр | ✅ только просмотр | ✅ только просмотр | ✅ KPI всех в салоне | ✅ KPI всей сети |
| Чужие KPI | ❌ | ❌ | ❌ | ✅ свой салон | ✅ |
| Оставлять оценки (`ratings`) | ✅ замерщика и сборщика | ✅ менеджера | ✅ менеджера | ✅ просмотр всех (видит `authorId`) | ✅ просмотр всех (видит `authorId`) |
| Получать оценки | ✅ от замерщика/сборщика | ✅ от менеджера и клиента | ✅ от менеджера и клиента | — | — |
| Видеть `authorId` оценки | ❌ (только итог) | ❌ (только итог) | ❌ (только итог) | ✅ | ✅ |
| Создание `requests` | ✅ создаёт | ✅ создаёт | ✅ создаёт | ✅ обрабатывает свой салон | ✅ видит эскалации |
| `shiftRequests` | ✅ создаёт свои | ✅ создаёт свои | ✅ создаёт свои | ✅ одобряет/отклоняет в салоне | ✅ просмотр |
| `config` / `commissionRules` | ❌ | ❌ | ❌ | ❌ просмотр своих ставок | ✅ полное управление |
| Управление `users` | ❌ | ❌ | ❌ | ✅ сотрудники своего салона | ✅ все, включая админов |
| Управление `salons` | ❌ | ❌ | ❌ | ❌ свой салон (чтение) | ✅ полное |
| Сводка по сети (все салоны) | ❌ | ❌ | ❌ | ❌ только свой салон | ✅ |
> Замерщик и сборщик видят только свои назначенные заказы (конкретный этап) и своё расписание; финансовые данные заказа (сумма, комиссия) им недоступны. Оценки оставляют и получают по правилам раздела 1.8 и БП-1315.
> Разграничение в первом прототипе — на уровне фильтрации в UI по `currentUser.role` и `currentUser.salonId`. Серверной проверки нет (бэкенда нет) — это известное ограничение прототипа, не передавать в прод без авторизации на сервере.
---
## 8. Скелет `data.js` (порядок и форма)
```js
var _DICT = { role:{...}, orderStatus:{...}, ratingStage:{...}, ratingCriteria:{...}, /* ... разделы 3.13.14 */ };
var config = {
kpiNorm:80, currency:'RUB', timezone:'Europe/Moscow',
workdayStart:'10:00', workdayEnd:'20:00', slotMinutes:60,
commissionRules:[ {role:'manager', rate:0.07} ] // дефолт 7%
};
var salons = [ /* раздел 1.1 */ ];
var users = [ /* раздел 1.2 */ ];
var clients = [ /* раздел 1.3 */ ];
var orders = [ /* раздел 1.4 — включая commissionRate */ ];
var appointments = [ /* раздел 1.5 */ ];
var requests = [ /* раздел 1.6 */ ];
var shiftRequests = [ /* раздел 4 */ ];
var ratings = [ /* раздел 1.8 */ ];
// Вычисляемые (раздел 5) — функции, НЕ поля:
function getMgrStats(userId, period){ /* visits/deals/revenue/conversion/avg/kpi */ }
function getSalonStats(salonId, period){ /* orders/revenue/overdue/... */ }
function isOverdue(order){ /* dueDate + status + сегодня */ }
function getFreeSlots(userId, date){ /* сетка минус appointments */ }
function getCommission(userId, period){ /* БП-5: orders.commissionRate → users.commissionRate → config */ }
function employeeRating(userId, period){ /* среднее scores по ratings.targetId=userId */ }
function orderRating(orderId){ /* клиентская оценка: ratings stage='order', authorId=clientId */ }
```
---
**Конец контракта.** При расхождении кода и этого документа — правится код. При расхождении документа и реальности — заводится правка документа, не молчаливый обход.