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 (порядок и форма)
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 */ }
Конец контракта. При расхождении кода и этого документа — правится код. При расхождении документа и реальности — заводится правка документа, не молчаливый обход.