mirror of
https://github.com/wasrusgen/wasrusgen1-crm.git
synced 2026-06-03 17:44:46 +00:00
529 lines
41 KiB
Markdown
529 lines
41 KiB
Markdown
# 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}` | Набор критериев → оценка 1–10 (целое). Ключи зависят от `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).
|
||
|
||
**Шкала:** 1–10 (целое). Среднее арифметическое по критериям `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 и БП-13–15.
|
||
|
||
> Разграничение в первом прототипе — на уровне фильтрации в UI по `currentUser.role` и `currentUser.salonId`. Серверной проверки нет (бэкенда нет) — это известное ограничение прототипа, не передавать в прод без авторизации на сервере.
|
||
|
||
---
|
||
|
||
## 8. Скелет `data.js` (порядок и форма)
|
||
|
||
```js
|
||
var _DICT = { role:{...}, orderStatus:{...}, ratingStage:{...}, ratingCriteria:{...}, /* ... разделы 3.1–3.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 */ }
|
||
```
|
||
|
||
---
|
||
|
||
**Конец контракта.** При расхождении кода и этого документа — правится код. При расхождении документа и реальности — заводится правка документа, не молчаливый обход.
|