KNOTTA research & development

Возможности платформы

Полный список функциональных возможностей 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Создание нарядаСтартовая позиция
startedPOST /service-visits/{id}/started/Бригада приступила. Отправляется WORK_STARTED подписчикам.
completedPOST /service-visits/{id}/completed/Работы завершены. Отправляется WORK_COMPLETED.
cancelledPOST /service-visits/{id}/cancelled/Наряд отменён. Допустим из любого статуса, кроме 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)

Шаблоны отчётов

Шаблон (ReportSurveyTemplate) — JSON-схема SurveyJS. У каждого шаблона есть имя, версия, описание и флаг is_default. Только один шаблон в системе может быть is_default=True (constraint в БД).

При создании наряда без явного шаблона — подставляется текущий default. При изменении default-шаблона уже созданные наряды не затрагиваются (они хранят свой report_template_id).

Справочники

СправочникМодельПрефиксНазначение
МатериалыMaterialmtrl_Расходные материалы (грунт, мульча, удобрения) с SKU и единицами измерения
Цены материаловMaterialPriceИстория цен с effective_from
УслугиServicesrv_Виды работ (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_startedServiceVisit.statusstartedadmin/staff + контактные лица клиента
work_completedServiceVisit.statuscompletedadmin/staff + контактные лица клиента
report_submittedReport.statussubmittedadmin/staff
report_approvedReport.statusapprovedбригадир-автор
report_readyсотрудник нажал «Отправить клиенту»контактные лица клиента
report_rejectedReport.statusrejectedбригадир-автор

Подробнее о маршрутизации, ролевых умолчаниях и расширении — в разделе Архитектура → Уведомления.

Доставка и её статусы

Каждая попытка доставки уведомления фиксируется в 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 — кнопку запуска веб-приложения прямо из бота.

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_* с пользователем и таймстемпом — это даёт полную прослеживаемость кто, что и когда менял.

На странице