⟨/⟩ Nts/50_Domain/VehicleSummaryStatistics.cs

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

using System;
using Rugrid.Sdaid.Notation.Documents;

namespace Rugrid.Sdaid.Epas.Nts.Domain
{
    /// <summary>
    /// Сводная статистика ТС: агрегирующий read-model по реестру ТС, не хранимая сущность —
    /// вычисляется по запросу из уже сохранённых «Транспортное средство» и «Расчёт ТС».
    /// Два разреза: состояние обработки (количество ТС по каждому значению справочника «Статус
    /// обработки ТС») и потребление топлива (по видам топлива, по нормативному расходу из
    /// успешных расчётов). Опциональный фильтр по организационному объекту — тот же принцип
    /// «объект и, при включённом «С потомками», подчинённые объекты», что и в реестре ТС.
    /// </summary>
    public class DOC_VehicleSummaryStatistics : IDomainEntityDocument, IHasStructuralLinks, IFolder<NtsDomainFolder>
    {
        public SdaidLink[] Links => new[]
        {
            Rel.Uses<DOC_Vehicle>(),
            Rel.Uses<DOC_VehicleCalc>(),
            Rel.Uses<DOC_ProcessingStatus>(),
            Rel.Uses<DOC_CalculationStatus>(),
            Rel.Uses<DOC_FuelKind>(),
            Rel.Uses<DOC_NavEntity>(),
        };

        public static string _refName = "Сводная статистика ТС";

        public string Name => "Сводная статистика ТС";

        public string Description =>
            @"Агрегирующий read-model по реестру ТС: состояние обработки (количество ТС по каждому статусу обработки) и потребление топлива в разрезе вида топлива (нормативный расход — сумма, максимум, минимум, среднее). Не хранимая сущность, вычисляется по запросу из уже сохранённых данных.";

        public string Version => "0.1";
        public string Status => "draft";
        public string[] Comments => new string[0];

        public Type Entity => typeof(VehicleSummaryStatistics);

        public static string Purpose = $""""
            ## Назначение

            Сводная статистика — не хранимая сущность, а форма результата агрегирующего запроса
            по реестру ТС. Вычисляется по запросу (не отчётный движок, не фоновая задача), читает
            уже сохранённые {SdaidAnchors.RefTo<DOC_Vehicle>()} и {SdaidAnchors.RefTo<DOC_VehicleCalc>()},
            никаких собственных данных не хранит и ничего не пересчитывает.

            Используется в двух местах: на главной странице модуля (без фильтра, по всему реестру
            ТС) и на вкладке «Сведения» организационного объекта (с фильтром по этому объекту).
            Оба места — один и тот же read-model с разным значением входного фильтра, см.
            {SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Ui.DOC_VehicleSummaryWidget>()}.

            ## Фильтр по организационному объекту

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

            ## Группа 1 — состояние обработки

            `TotalCount` — общее количество ТС, отобранных фильтром. `ByProcessingStatus` — одна
            строка {SdaidAnchors.RefTo<DOC_VehicleProcessingStatusCount>()} на каждое значение
            справочника {SdaidAnchors.RefTo<DOC_ProcessingStatus>()} (состав — там, не повторяется
            здесь) — строка выводится всегда, даже если по этому статусу нет ни одного ТС
            (`Count = 0`), чтобы состав плашек не менялся от выборки к выборке.

            {SdaidTable.OfClass<VehicleProcessingStatusCount>()}

            ## Группа 2 — потребление топлива, в разрезе вида топлива

            `ByFuelKind` — одна строка {SdaidAnchors.RefTo<DOC_VehicleFuelConsumptionSummary>()} на
            каждое значение {SdaidAnchors.RefTo<DOC_FuelKind>()}, встретившееся среди ТС, отобранных
            фильтром (бензин/дизельное топливо; литры разных видов топлива не складываются — два
            самостоятельных разреза). `VehicleCount` — количество ТС этого вида топлива с успешным
            расчётом (`CalculationStatus` = «Успешно выполнен»), участвующих в агрегации. Нормативный
            расход (`NormConsumptionSum/Max/Min/Avg`) — сумма, максимум, минимум и среднее по полю
            `VehicleCalc.NormConsumption` (итоговый результат расчёта $Q_н$,
            {SdaidAnchors.RefTo<DOC_VehicleCalc>()}) этих ТС; источник — уже сохранённый результат
            расчёта, агрегация не обращается к `InputModel` (контракту входа расчёта, существующему
            только во время его выполнения) и не запускает расчёт повторно. `ActualConsumption` —
            зарезервировано на будущее: в домене ЭНТС сейчас нет источника фактических данных по
            топливу ТС; поле не вычисляется и не выводится, пока источник не появится. Не путать с
            «Фактическое потребление» модуля ЭНЗС (Ntr) — там про тепло/электричество зданий, другая
            модель данных, другой модуль.

            {SdaidTable.OfClass<VehicleFuelConsumptionSummary>()}
            """";
    }

    /// <summary>
    /// Доменная сущность «Сводная статистика ТС» — верхнеуровневая форма ответа: фильтр и общее
    /// количество ТС. Разрезы — отдельные построчные формы: <see cref="VehicleProcessingStatusCount"/>
    /// и <see cref="VehicleFuelConsumptionSummary"/>.
    /// </summary>
    [DomainEntity]
    public class VehicleSummaryStatistics
    {
        /// <summary>Организационный объект — фильтр</summary>
        [Relation]
        [DomainField(Alias = "org_object_id",
            Description = "Не задан — статистика по всему реестру ТС; задан — по объекту и, при «С потомками», подчинённым объектам")]
        public NavEntity? OrgObject { get; set; }

        /// <summary>Всего ТС</summary>
        [DomainField(Alias = "total_count", Range = "целое, ≥ 0",
            Calculated = "Да, количество ТС, отобранных фильтром `OrgObject`")]
        public int TotalCount { get; set; }
    }

    /// <summary>Доменная сущность «Количество ТС по статусу обработки» — строка группы 1.</summary>
    public class DOC_VehicleProcessingStatusCount : IDomainEntityDocument, IFolder<NtsDomainFolder>
    {
        public static string _refName = "Количество ТС по статусу обработки";

        public string Name => "Количество ТС по статусу обработки";

        public string Description =>
            @"Строка сводной статистики: количество ТС выбранной выборки с данным статусом обработки.";

        public string Version => "0.1";
        public string Status => "draft";
        public string[] Comments => new string[0];

        public Type Entity => typeof(VehicleProcessingStatusCount);
    }

    [DomainEntity]
    public class VehicleProcessingStatusCount
    {
        /// <summary>Статус обработки</summary>
        [Relation]
        [DomainField(Alias = "processing_status_id", RequiredOnCreate = true)]
        public ProcessingStatus? ProcessingStatus { get; set; }

        /// <summary>Количество ТС</summary>
        [DomainField(Alias = "count", Range = "целое, ≥ 0",
            Calculated = "Да, количество ТС выборки с этим статусом обработки")]
        public int Count { get; set; }
    }

    /// <summary>Доменная сущность «Потребление топлива по виду топлива» — строка группы 2.</summary>
    public class DOC_VehicleFuelConsumptionSummary : IDomainEntityDocument, IFolder<NtsDomainFolder>
    {
        public static string _refName = "Потребление топлива по виду топлива";

        public string Name => "Потребление топлива по виду топлива";

        public string Description =>
            @"Строка сводной статистики: агрегаты нормативного расхода топлива ТС выбранной выборки с данным видом топлива; фактический расход — зарезервированное поле.";

        public string Version => "0.1";
        public string Status => "draft";
        public string[] Comments => new string[0];

        public Type Entity => typeof(VehicleFuelConsumptionSummary);
    }

    [DomainEntity]
    public class VehicleFuelConsumptionSummary
    {
        /// <summary>Вид топлива</summary>
        [Relation]
        [DomainField(Alias = "fuel_kind_id", RequiredOnCreate = true,
            Description = "Vehicle.FuelKindPrimary — основной вид топлива ТС")]
        public FuelKind? FuelKind { get; set; }

        /// <summary>Количество ТС с успешным расчётом</summary>
        [DomainField(Alias = "vehicle_count", Range = "целое, ≥ 0",
            Calculated = "Да, количество ТС этого вида топлива с CalculationStatus = «Успешно выполнен»")]
        public int VehicleCount { get; set; }

        /// <summary>Нормативный расход — сумма, л</summary>
        [DomainField(Alias = "norm_consumption_sum", Unit = "л", Range = "вещественное, ≥ 0",
            Calculated = "Да, сумма VehicleCalc.NormConsumption по ТС этого вида топлива с успешным расчётом")]
        public decimal NormConsumptionSum { get; set; }

        /// <summary>Нормативный расход — максимум, л</summary>
        [DomainField(Alias = "norm_consumption_max", Unit = "л", Range = "вещественное, ≥ 0",
            Calculated = "Да, максимум VehicleCalc.NormConsumption по той же выборке")]
        public decimal NormConsumptionMax { get; set; }

        /// <summary>Нормативный расход — минимум, л</summary>
        [DomainField(Alias = "norm_consumption_min", Unit = "л", Range = "вещественное, ≥ 0",
            Calculated = "Да, минимум VehicleCalc.NormConsumption по той же выборке")]
        public decimal NormConsumptionMin { get; set; }

        /// <summary>Нормативный расход — среднее, л</summary>
        [DomainField(Alias = "norm_consumption_avg", Unit = "л", Range = "вещественное, ≥ 0",
            Calculated = "Да, среднее VehicleCalc.NormConsumption по той же выборке")]
        public decimal NormConsumptionAvg { get; set; }

        /// <summary>Фактический расход, л — зарезервировано</summary>
        [DomainField(Alias = "actual_consumption", Unit = "л",
            Description = "Зарезервировано на будущее: источник фактических данных по топливу ТС в домене ЭНТС не определён; поле не вычисляется")]
        public decimal? ActualConsumption { get; set; }
    }
}