⟨/⟩ Nts/35_ChangeRequests/DOC_CR_2026_10_30_VehicleSummaryStatistics.cs

184 строк · в начало

using Rugrid.Sdaid.Notation.Documents;

namespace Rugrid.Sdaid.Epas.Nts.ChangeRequests
{
    /// <summary>
    /// Новое требование: сводная статистика по реестру ТС, выводится плашками в двух местах —
    /// на вкладке «Сведения» организационного объекта (с фильтром по объекту) и на главной
    /// странице модуля (без фильтра, по всему реестру).
    /// </summary>
    public class DOC_CR_2026_10_30_VehicleSummaryStatistics : IChangeRequestDocument, IHasStructuralLinks, IFolder<NtsChangeRequestsFolder>
    {
        public SdaidLink[] Links => new[]
        {
            Rel.Uses<Rugrid.Sdaid.Epas.Nts.Domain.DOC_VehicleSummaryStatistics>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Nts.Ui.DOC_VehicleSummaryWidget>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Nts.Ui.DOC_NtsMainPage>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Nts.Ui.DOC_VehicleRegistry>(),
        };

        public static string _refName = "CR 2026-10-30: сводная статистика ТС (плашки)";

        public string Name => "Сводная статистика ТС (плашки)";

        public string Description =>
            @"Сводная статистика по реестру ТС — состояние обработки и потребление топлива в разрезе вида топлива, выводится плашками на главной странице модуля и на вкладке «Сведения» организационного объекта.";

        public string Version => "0.4";
        public string Status => "done";
        public string[] Comments => new string[0];

        public static string Source = """"
            ## Источник (как есть)

            В модуле ЭНТС нет сводного представления состояния реестра ТС. Чтобы увидеть, сколько
            ТС находится в каждом статусе обработки или каков суммарный нормативный расход топлива,
            нужно открывать реестр ТС и просматривать строки вручную — нет агрегированной сводки.

            Согласованы метрики двух групп:

            1. **Состояние обработки.** Всего ТС и разбивка по каждому из семи значений справочника
               «Статус обработки ТС»: Не выполнена, Мало данных загружено, Нет модификаций,
               Отсутствуют данные для расчёта, Автовыбор не подтверждён, Модификация не определена,
               Завершена.
            2. **Потребление топлива ТС, в разрезе вида топлива.** Для каждого вида топлива
               (бензин/дизельное топливо — не складываются, два отдельных разреза): нормативный
               расход (сумма, максимум, минимум, среднее) из уже сохранённого поля
               `VehicleCalc.NormConsumption` по ТС этого вида топлива с успешным расчётом;
               фактический расход — зарезервированное поле на будущее, источник не определён.

            Место вывода: плашки на вкладке «Сведения» организационного объекта (фильтр — ТС этого
            объекта) и плашки на главной странице модуля (без фильтра, по всему реестру ТС).
            """";

        public static string Analysis = $$""""
            ## Разбор

            **Технология.** Не отчётный движок (ClosedXML и т. п. — для скачиваемых xlsx, не для
            веб-плашек): обычный агрегирующий backend-эндпоинт/read-model + UI-виджет. Отдельная
            задача на будущее — если понадобится скачиваемый xlsx-отчёт по этим же метрикам,
            переиспользовать паттерн `Ntr.Reports`/`NtrReportApiController` (сейчас не проектируется).

            **Форма read-model.** Статистика не хранимая сущность — вычисляется по запросу из уже
            сохранённых {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Domain.DOC_Vehicle>()}} и
            {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Domain.DOC_VehicleCalc>()}}. По аналогии с
            `OutputModel` (форма результата калькулятора, не хранимая сущность,
            {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Backend.Calc.DOC_OutputModel>()}}) заведена
            новая сущность {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Domain.DOC_VehicleSummaryStatistics>()}}
            с двумя построчными формами разрезов — по статусу обработки и по виду топлива — каждая
            из которых документирована отдельным `[DomainEntity]`-классом и встроена в тело основного
            документа через `SdaidTable.OfClass<T>()` (приём, уже применённый в
            `Nts/70_Infrastructure/AisOiaPeo/AisImportMaps.cs` для карт приёма данных АИС).

            **Фильтр по организационному объекту.** Переиспользован принцип
            {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Ui.DOC_VehicleRegistry>()}}: объект задаёт
            организационный фильтр, при включённом «С потомками» — включаются подчинённые объекты
            (ТС числятся за МЭС и ПМЭС). На вкладке «Сведения» отдельного переключателя «С
            потомками» нет — сводка по объекту всегда считается с подчинёнными; на главной странице
            модуля фильтр не задаётся вовсе.

            **Почему не use-case.** Виджет — агрегирующее чтение в рамках одного модуля без
            фонового процесса и без состояния, сохраняемого между шагами; значит, по методологии —
            поведение страницы/компонента, не use-case.

            **Почему «Фактический» не проектируется как рабочее поле.** В домене ЭНТС сейчас нет
            источника фактических данных по топливу ТС. «Фактическое потребление» ЭНЗС (Ntr) — про
            тепло/электричество зданий, другая модель данных и другой модуль; переиспользовать её
            как источник для ЭНТС нельзя. Поле `ActualConsumption` зафиксировано в структуре как
            зарезервированное (nullable, без `Calculated`), но не вычисляется и не выводится в
            виджете, пока источник не определён — чтобы в будущем расширить read-model без изменения
            состава уже существующих полей.
            """";

        public static string BlastRadius = $""""
            ## Blast radius

            ### Спецификация

            | Документ | Правка |
            |---|---|
            | `Nts/50_Domain/VehicleSummaryStatistics.cs` (новый) | {SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Domain.DOC_VehicleSummaryStatistics>()} — read-model, две построчные формы разрезов |
            | `Nts/65_Ui/VehicleSummaryWidget.cs` (новый) | {SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Ui.DOC_VehicleSummaryWidget>()} — UI-компонент плашек, встраиваемый в двух местах |
            | {SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Ui.DOC_NtsMainPage>()} | добавлена ссылка на виджет; раздел «Структура экрана» — плашки без фильтра до выбора объекта, плашки с фильтром на вкладке «Сведения» |

            ### Прототип

            | Файл | Правка |
            |---|---|
            | `Nts/65_Ui/html/main-page.html` | блок плашек без фильтра (по всему реестру ТС), встроенный в разметку главной страницы |
            | орг.объектная страница/вкладка «Сведения» (найти по разметке `VehicleRegistry`/оргструктуры в `Nts/65_Ui/html/`) | блок плашек с фильтром по текущему орг. объекту |
            | `Ntr/65_Ui/html/shell.js` | функция агрегации по данным `NTS_VEHICLE`/`ensureNtsState` (количество по статусу обработки; сумма/макс/мин/среднее `NormConsumption` по виду топлива для ТС с успешным расчётом) + функция рендера плашек; вызовы в обоих местах вывода |
            | `Ntr/65_Ui/html/shell.css` | стили плашек (переиспользовать существующие паттерны карточек/бейджей, не изобретать новый визуальный язык) |

            Прототип — часть того же CR, не отдельный шаг «потом»: спека без рабочего прототипа,
            который `testovich` может сверить выполнением, не считается законченной итерацией.
            """";

        public static string Plan = """"
            ## План

            1. Завести доменную сущность «Сводная статистика ТС» с двумя построчными формами
               разрезов («Количество ТС по статусу обработки», «Потребление топлива по виду
               топлива»), структурные связи на `Vehicle`, `VehicleCalc`, справочники.
            2. Завести UI-компонент «Плашки сводной статистики ТС» с описанием структуры (два ряда
               плашек) и поведения (фильтр внешний, компонент только отображает).
            3. Обновить `NtsMainPage.cs`: ссылка на компонент, упоминание плашек в обоих местах
               рабочей области (до выбора объекта — без фильтра; вкладка «Сведения» — с фильтром).
            4. Собрать (`dotnet build`, `build-html.cmd`), проверить отсутствие документов-сирот.
            5. Прототип: функция агрегации по демо-данным (`shell.js`) + разметка плашек в обоих
               местах вывода (`main-page.html`, вкладка «Сведения» орг. объекта) + стили.
            6. `testovich` — реальным выполнением на нескольких орг. объектах/без фильтра сверить
               подсчёт плашек с фактическим составом демо-ТС.
            7. Зафиксировать «Итог» по факту проверки.
            """";

        public static string Acceptance = """"
            ## Acceptance criteria

            - **AC-1.** В спеке описан read-model «Сводная статистика ТС» с полями: общее
              количество ТС, количество по каждому из семи статусов обработки (строка выводится
              даже при нулевом количестве), и по каждому виду топлива — количество ТС с успешным
              расчётом, сумма/максимум/минимум/среднее нормативного расхода.
            - **AC-2.** Поле фактического расхода зафиксировано как зарезервированное, без
              источника данных и без вычисления в этой итерации.
            - **AC-3.** Описан фильтр по организационному объекту, переиспользующий принцип фильтра
              реестра ТС (объект + подчинённые объекты).
            - **AC-4.** Описаны оба места вывода: главная страница модуля (без фильтра) и вкладка
              «Сведения» организационного объекта (с фильтром), один и тот же UI-компонент.
            - **AC-5.** Сборка спеки (`dotnet build`) и генерация сайта (`build-html.cmd`) проходят
              без ошибок, новые документы не являются сиротами.
            - **AC-6.** Прототип реализован в обоих местах вывода и подтверждён `testovich` реальным
              выполнением: подсчёт плашек (по статусам обработки и по виду топлива) совпадает с
              фактическим составом демо-ТС — без фильтра и с фильтром по орг. объекту.
            - **AC-7.** На вкладке «Сведения» виджет размещён отдельным блоком после полей сведений
              об организационном объекте, не между ними. Когда сумма ТС по видам топлива (плашка 2)
              меньше «Всего ТС» (плашка 1) — остаток явно показан строкой «Без успешного расчёта: N
              ТС», а не оставлен несходящимся молча.
            - **AC-8.** Виджет — ровно **две плашки** (не десять): плашка 1 «Состояние обработки»
              содержит строку «Всего ТС» и все 7 статусов как строки внутри неё; плашка 2
              «Потребление автотоплива ТС» содержит по строке на каждый вид топлива. Разбивка —
              строки внутри плашки своей группы, не отдельная плашка на каждое значение.
            """";

        public static string Result = """"
            ## Итог

            Реализовано и проверено `testovich` реальным выполнением. Read-model
            {SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Domain.DOC_VehicleSummaryStatistics>()} и
            виджет {SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Ui.DOC_VehicleSummaryWidget>()}
            зафиксированы в спеке, прототип — в обоих местах вывода (главная страница без фильтра,
            вкладка «Сведения» с фильтром по орг. объекту). По итогам ревью пользователя доведены
            до соответствия: группировка по виду топлива — только бензин/дизель (как и методика
            расчёта), ряд 2 скрывается лишь при пустой выборке, добавлена строка сверки «Без
            успешного расчёта: N ТС», виджет размещён после полей сведений об объекте, не между
            ними. AC-1 — AC-7 подтверждены. По итогам ещё одного ревью пользователя обнаружено:
            «ряд»/«группа» были реализованы как множество отдельных плашек (10 штук — по одной на
            статус/вид топлива), хотя имелось в виду ровно 2 плашки с разбивкой строками внутри
            каждой (AC-8). Переделано: `summaryTilesHtml()` строит 2 `.summary-group`, разбивка —
            `.summary-row` внутри; спека и CSS согласованы. AC-8 подтверждён `testovich` реальным
            выполнением (несколько орг. объектов, ручной пересчёт сумм/мин/макс/среднего, случай
            пустой выборки — вторая плашка действительно отсутствует в DOM). AC-1 — AC-8 подтверждены.
            """";
    }
}