Возможности платформы
Полный список функциональных возможностей NG-Metrics — от управления клиентами до публичных отчётов и уведомлений.
Возможности платформы
Полная карта возможностей. Если вы ищете описание конкретной модели или эндпоинта — переходите в Модель данных или Для разработчиков → API.
Клиенты, контакты и объекты
Платформа ведёт базу клиентов с разделением на физические лица (ClientType.INDIVIDUAL) и организации (ClientType.ORGANIZATION). Для организации хранится ИНН (tax_id), для всех — контактные данные и адрес.
У каждого клиента может быть произвольное количество контактных лиц (ContactPerson, префикс cprsn_). Контактное лицо — это конкретный человек со стороны клиента, с которым ведётся переписка и которому отправляются уведомления. На каждом контактном лице хранится email, телефон, должность и предпочитаемый язык.
Объекты клиента (ClientObject, префикс cobj_) — физические территории, на которых выполняются работы. У одного клиента может быть несколько объектов, у каждого — название и адрес. Все наряды привязаны к конкретному объекту.
Приглашения клиентов
Чтобы предоставить контактному лицу доступ в личный кабинет, администратор или сотрудник создаёт приглашение (ClientInvite, префикс cinvt_). Приглашение содержит уникальный токен и фиксирует email на момент отправки (snapshot).
Приглашения проходят через статусы:
| Статус | Что значит |
|---|---|
pending | Создано, ссылка отправлена, ждёт принятия |
accepted | Получатель прошёл по ссылке и зарегистрировался — учётка привязана к контактному лицу |
revoked | Отозвано до принятия |
expired | Истёк срок (expires_at) |
Канал доставки (channel) — email, telegram или прямая ссылка — фиксируется в момент отправки.
Наряды на обслуживание
Наряд (ServiceVisit, префикс sv_) — центральная сущность платформы. Описывает один выезд бригады на объект клиента.
При создании наряда указывается:
- Объект клиента (
object) — на какой территории работаем. - Бригадир (
brigadier) — кто отвечает за выезд. - Шаблон отчёта (
report_template) — какой SurveyJS-шаблон будет использоваться. Если не указан явно — подставляется системный шаблон по умолчанию. - Плановая дата (
planned_for). - Состав бригады (
ServiceVisitMember) — необязательно; работники могут быть добавлены позже или появятся автоматически из отчёта при его утверждении.
Жизненный цикл наряда:
planned ─────▶ started ─────▶ completed
│ │
▼ ▼
cancelled cancelled| Статус | Триггер | Что происходит |
|---|---|---|
planned | Создание наряда | Стартовая позиция |
started | POST /service-visits/{id}/started/ | Бригада приступила. Отправляется WORK_STARTED подписчикам. |
completed | POST /service-visits/{id}/completed/ | Работы завершены. Отправляется WORK_COMPLETED. |
cancelled | POST /service-visits/{id}/cancelled/ | Наряд отменён. Допустим из любого статуса, кроме completed. |
В текущей версии у ServiceVisit нет отдельного статуса «на проверке». Цикл рецензирования живёт на стороне отчёта (Report.status), а наряд переходит в completed сразу после действия completed — логически независимо от статуса связанного отчёта.
Отчёты
К каждому наряду привязан ровно один отчёт (Report, префикс rprt_, связь OneToOneField). Автор отчёта — бригадир (Report.author); рецензент — admin или staff (Report.reviewer).
Шаблон отчёта (ReportSurveyTemplate, префикс rprts_) описывает структуру формы в формате SurveyJS. В момент создания отчёта снимок шаблона записывается в три поля Report.template_*_snapshot. Это гарантирует, что обновление шаблона не сломает уже сданные отчёты.
Жизненный цикл отчёта:
draft ─▶ submitted ─▶ approved
▲ │
│ ▼
rejected ─────── (back to brigadier)| Статус | Что значит |
|---|---|
draft | Отчёт создан, форма не заполнена / не отправлена |
submitted | Бригадир подал отчёт на рецензию. Отправляется REPORT_SUBMITTED сотрудникам. |
approved | Рецензент утвердил. Отправляется REPORT_APPROVED бригадиру. |
rejected | Рецензент отклонил с комментарием (rejection_comment). Отправляется REPORT_REJECTED бригадиру. |
После утверждения сотрудник нажимает «Отправить клиенту» (POST /reports/{id}/survey/send-to-client/) — это поднимает флаг Report.sent_to_client и отправляет уведомление REPORT_READY контактным лицам.
Поле submission_attempts инкрементируется при каждом переходе draft|rejected → submitted — это даёт метрику качества первой подачи.
К отчёту можно прикреплять файлы (FileAttachment, префикс file_) — фотографии, документы. Файлы хранятся в Supabase Storage; URL не хранится в БД, генерируется на лету через подписанные ссылки.
Учёт работ и человеко-часов
Внутри отчёта (в survey_data) бригадир указывает выполненные работы. Платформа поддерживает два вида работ из коробки: стандартный уход и топиарная стрижка. Для каждого вида:
- Чекбокс активации (
is-routine-care,is-topiary-cutting). - Динамическая таблица работников (
routine-care-workers,topiary-cutting-workers) — каждая строка содержитworker_idиhours.
Работник выбирается из общего списка пользователей с ролью worker через эндпоинт /api/workers/ (lazy loading в SurveyJS).
При утверждении отчёта (POST /reports/{id}/survey/approve/) работники из survey_data автоматически синхронизируются с ServiceVisitMember (роль worker): добавляются недостающие, удаляются лишние, существующие сохраняются. Операция идемпотентна — повторное утверждение приводит состав бригады в соответствие с актуальным отчётом.
Расчёт человеко-часов в итоговом отчёте:
Человек = COUNT(workers)
Часы = SUM(workers[].hours)Отчёты, созданные до апрельской модификации 2026 года, использовали слайдеры *-person-count и *-hours-count. Расчёт автоматически определяет формат данных: если в survey_data есть массив *-workers — новая логика, если есть *-person-count — people × hours. Это позволяет старые отчёты продолжать работать без миграции данных.
Шаблоны отчётов
Шаблон (ReportSurveyTemplate) — JSON-схема SurveyJS. У каждого шаблона есть имя, версия, описание и флаг is_default. Только один шаблон в системе может быть is_default=True (constraint в БД).
При создании наряда без явного шаблона — подставляется текущий default. При изменении default-шаблона уже созданные наряды не затрагиваются (они хранят свой report_template_id).
Справочники
| Справочник | Модель | Префикс | Назначение |
|---|---|---|---|
| Материалы | Material | mtrl_ | Расходные материалы (грунт, мульча, удобрения) с SKU и единицами измерения |
| Цены материалов | MaterialPrice | — | История цен с effective_from |
| Услуги | Service | srv_ | Виды работ (Routine care, Topiary cutting) с slug, единицами и категорией |
| Цены услуг | ServicePrice | — | История цен на услуги |
| Расход материалов на наряде | ServiceVisitMaterial | — | Какие материалы были использованы на конкретном наряде |
| Услуги на наряде | ServiceVisitService | — | Какие услуги были выполнены на конкретном наряде |
Подсистема уведомлений
С апреля 2026 в платформе работает выделенный модуль modules/notification. Подписки и каналы хранятся в единой модели NotificationPreference (префикс ntfp_). Подписчиком может быть либо User (сотрудник), либо ContactPerson (контактное лицо клиента) — никогда оба одновременно (CHECK-ограничение в БД).
Поддерживаемые каналы (DeliveryChannel):
telegram— через Redis Stream → Telegram-бот.email— напрямую через Resend API из Django (MJML-шаблоны).
Поддерживаемые топики (NotificationTopic):
| Топик | Когда отправляется | Кому |
|---|---|---|
work_started | ServiceVisit.status → started | admin/staff + контактные лица клиента |
work_completed | ServiceVisit.status → completed | admin/staff + контактные лица клиента |
report_submitted | Report.status → submitted | admin/staff |
report_approved | Report.status → approved | бригадир-автор |
report_ready | сотрудник нажал «Отправить клиенту» | контактные лица клиента |
report_rejected | Report.status → rejected | бригадир-автор |
Подробнее о маршрутизации, ролевых умолчаниях и расширении — в разделе Архитектура → Уведомления.
Доставка и её статусы
Каждая попытка доставки уведомления фиксируется в ReportDelivery (префикс rprtdel_). Запись несёт информацию о канале, топике, получателе и текущем статусе.
Жизненный цикл доставки (DeliveryStatus):
pending → sending → {sent, failed, expired, cancelled}
↓
{delivered, opened, bounced, complained}Telegram сообщает только sent / failed. Resend сообщает полную цепочку через webhook — вплоть до opened (по tracking pixel).
Webhook-приёмники:
POST /api/webhooks/telegram/<delivery_id>— от Telegram-бота (HMAC-SHA256).POST /api/webhooks/resend/— от Resend (Svix-подпись).POST /api/report-deliveries/<delivery_id>/webhook— устаревший алиас Telegram-приёмника, оставлен для совместимости со старыми сборками бота.
Публичный просмотр отчётов
Утверждённые отчёты доступны по прямой ссылке без авторизации: /{locale}/report/{report_id}. Эту ссылку можно отправить любому заинтересованному лицу — она работает в обе стороны (на ru/en).
Email-уведомление report_ready содержит именно эту ссылку.
Telegram-бот
Бот (bot/) — отдельный сервис на Aiogram 3.20 + FastAPI + SQLAlchemy. Реализует:
- Привязку Telegram-аккаунта к пользователю через deep-link с токеном.
- Доставку уведомлений через подписку на Redis Stream.
- Двунаправленную поддержку через интеграцию с Chatwoot (входящие в Telegram → инцидент в Chatwoot, ответ оператора → отправка в Telegram).
- Mini App — кнопку запуска веб-приложения прямо из бота.
Из-за ограничений Telegram в РФ роли ng-metrics-bot и chatwoot в Ansible-плейбуке закомментированы и не разворачиваются на проде. Код сохранён в репозитории — для возврата достаточно раскомментировать соответствующие блоки в ansible/playbooks/prod/ng-metrics.yaml. Email-канал работает в полном объёме.
Email-уведомления
Платформа отправляет два типа писем.
Транзакционные (от Supabase Auth — приглашение клиента, сброс пароля, смена email): через Supabase Edge Function send-email, шаблоны на React Email (TSX).
Бизнес-уведомления (work_started, work_completed, report_*): напрямую из Django через Resend API, шаблоны на MJML (modules/notification/templates/notification/email/).
Письма двуязычные. Язык определяется по User.preferred_language или ContactPerson.preferred_language.
В каждом письме доступна отписка по одному клику — ссылка ведёт на /api/notifications/unsubscribe/execute/ со временным токеном, который выключает соответствующий NotificationPreference.
Аудит и история
Ключевые сущности (Client, ContactPerson, ClientObject, ServiceVisit, Report) ведут историю изменений через django-simple-history. Каждое обновление пишется в отдельную таблицу historical_* с пользователем и таймстемпом — это даёт полную прослеживаемость кто, что и когда менял.