⟨/⟩ Nts/65_Ui/ModificationSelectionModal.cs

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

using Rugrid.Sdaid.Notation.Documents;

namespace Rugrid.Sdaid.Epas.Nts.Ui
{
    /// <summary>
    /// Окно подбора модификаций ТС — модальное окно над карточкой ТС: отбор модификаций
    /// справочника «Марка, модель и модификация ТС» под конкретное ТС и расчёт базовой нормы.
    /// </summary>
    public class DOC_ModificationSelectionModal : IPageDocument, IHasStructuralLinks, IFolder<NtsUiFolder>
    {
        public SdaidLink[] Links => new[]
        {
            Rel.Uses<Rugrid.Sdaid.Epas.Nts.Domain.DOC_Vehicle>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Nts.Domain.DOC_VehicleCalc>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Nts.Domain.DOC_VehicleModification>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Nts.Domain.DOC_SelectedModifications>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Nts.Backend.Calc.DOC_ProcessingStatusPolicy>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Nts.Backend.Calc.DOC_AisRefreshPolicy>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Nts.Backend.ModificationSelection.DOC_ModificationGroupSelection>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Nts.Backend.ModificationSelection.DOC_ModificationNameRecognition>(),
            Rel.Uses<DOC_VehicleCard>(),
            Rel.Uses<DOC_VehicleModificationCard>(),
        };

        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];

        /// <summary>Макет экрана: живой прототип окна подбора модификаций.</summary>
        public static string Wireframe = $""""
        ## Макет

        {SdaidHtml.IframeOf<DOC_ModificationSelectionModal>("modification-selection.html", 620)}

        {SdaidHtml.LinkTo<DOC_ModificationSelectionModal>("modification-selection.html", "Открыть макет в отдельной вкладке")}

        Окно подбора открывается иконкой «Подобрать модификацию» у поля «Базовая норма расхода, л/100 км».
        """";
        public static string Structure = $$""""
        ## Структура экрана

        Модальное окно над карточкой ТС; открывается иконкой «Подобрать модификацию» у поля
        «Базовая норма расхода, л/100 км». Отбирает модификации справочника
        {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Domain.DOC_VehicleModification>()}} и
        определяет базовую норму расхода топлива Hs.

        ### Основные интерфейсные элементы

        | Элемент | Роль |
        |---|---|
        | Итоговая строка | Выводится в верхней части окна, над всеми блоками; выделена как результат работы окна. Результат секций `fuel.base_norm` и `fuel.reference_norm`: `H_min: X · H_max: Y · Hs: Z · H_эт: W л/100 км` |
        | Наименование ТС | Контекстная информация окна, только чтение |
        | Вид ТС | Контекстная информация окна, только чтение; задаётся ТС |
        | Вид топлива | Контекстная информация окна, только чтение; задаётся ТС |
        | Блок «Условия подбора модификаций» (маска) | Задаёт условия отбора в «Отфильтрованные модификации». Маска — значения трёх полей: Марка, Модель, Модификация |
        | — Марка | Выбор из марок справочника по виду ТС; «Все марки» — условие не задано |
        | — Модель | Выбор из моделей, доступных в группе после фильтра по марке; при смене марки список перестраивается; «Все модели» — условие не задано |
        | — Модификация | Подстрока по полному наименованию |
        | Блок «Отфильтрованные модификации» | Модификации, прошедшие условия; каждая строка — чекбокс «добавить в Отобранные» и «Выбрать все» |
        | — Сообщение при пустом отборе | «нет модификаций для Вид ТС «…» и Вид топлива «…»» — если для условий нет модификаций с установленной нормой |
        | Блок «Отобранные модификации» | Самостоятельный список, не зависит от текущей маски; «×» на строку — удалить, «Очистить» — удалить все |
        | Баннер «Модификация определена» | Зелёный, при одной отобранной |
        | Баннер «Группа модификаций определена» | Жёлтый, при нескольких отобранных |
        | Баннер «Модификация не определена» | Серый, при отсутствии отобранных |
        | Кнопка «Создать модификацию» | В подвале окна, всегда доступна (независимо от результата отбора — нужной модификации может не быть и при непустом списке); по правам пользователя (администратор модуля) |
        | Кнопки «Отменить», «Применить» | Закрытие без изменений / фиксация отобранного |

        ### Правила подбора

        1. **Текущая группа.** При открытии окна «Отобранные» заполняются один раз группой,
           уже подобранной для ТС — это разовое копирование, не постоянная синхронизация с
           «Отфильтрованными» (см. правило 3а). Если группу сохранил пользователь по
           «Применить», способ подбора — «эксперт»; если группа получена автоподбором при приёме
           данных ТС из АИС (первом или повторном) — «авто». Когда именно сервис выполняет этот
           автоподбор — см. {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Backend.Calc.DOC_AisRefreshPolicy>()}}.
        2. **Базовая группа.** Базовая группа строится при открытии всегда — по виду ТС и виду
           топлива. В неё входят все модификации вида ТС с установленной нормой; если по
           основному виду топлива такие модификации есть, группа сужается по топливу (модификации
           с пустым видом топлива остаются). Если таких модификаций нет, группа остаётся по виду
           ТС. Какое поле справочника считается установленной нормой — открытый вопрос
           {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Epics.NtsFuelCalc.OQ_BaseNorm>()}}; сам
           отбор — {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Backend.ModificationSelection.DOC_ModificationGroupSelection>()}},
           раздел «Базовая группа».
        3. **Маска.** Маска — значения полей Марка, Модель и Модификация; Вид ТС и Вид топлива —
           контекст ТС, только чтение, в маску не входят. Маска формируется заново **при каждом
           открытии окна**, только из текущих данных АИС («Марка и модель базы ТС») — не зависит
           ни от способа подбора, ни от того, что сохранено в «Отобранных». Если базовая группа
           пуста — маска пустая, автоуточнение не выполняется. Иначе — автоуточнение: марка —
           распознанная в «Марка и модель базы ТС» и присутствующая в базовой группе; модель —
           самая длинная модель группы после фильтра по марке, входящая в наименование ТС; если
           модель распознана, модификация не заполняется, иначе — остаток наименования без марки.
           Распознавание — {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Backend.ModificationSelection.DOC_ModificationNameRecognition>()}},
           композиция уточнения —
           {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Backend.ModificationSelection.DOC_ModificationGroupSelection>()}},
           раздел «Композиция уточнения». Маску нигде не нужно хранить — она не переживает
           закрытие окна, поэтому хранилища для неё в домене не требуется.
        3а. **Независимость «Отфильтрованных» и «Отобранных».** Это два самостоятельных списка.
           «Отфильтрованные» — результат применения текущей маски к базовой группе, пересчитывается
           при любом изменении маски. «Отобранные» — набор модификаций, собранный правилом 1 при
           открытии и далее меняющийся только явными действиями пользователя (правило 3б); смена
           маски состав «Отобранных» не меняет.
        3б. **Действия с отбором.** Чекбокс строки в «Отфильтрованных» только добавляет её в
           «Отобранные»; после добавления строка в «Отфильтрованных» блокируется (серая,
           недоступна для повторного действия) — повторно её можно отобрать снова только после
           удаления из «Отобранных». «Выбрать все» в «Отфильтрованных» — массовое добавление всех
           текущих строк, с той же блокировкой; отдельной кнопки «Снять» в «Отфильтрованных» нет —
           убрать уже отобранную запись можно только в блоке «Отобранные». «×» на строку в
           «Отобранных» удаляет её оттуда и разблокирует соответствующую строку в
           «Отфильтрованных», если она там всё ещё есть (под текущей маской); «Очистить» —
           то же самое для всех отобранных разом. Счётчик «Отфильтровано: N» включает
           заблокированные строки — N всегда равен числу строк, подошедших под текущую маску,
           независимо от того, отобраны они или нет.
        4. **Ручные условия.** Марка и Модель — точное совпадение; Модификация — подстрока
           полного наименования. Условие, заданное вручную, строгое: если оно даёт пустой
           результат, выборка пуста и показывается сообщение об отсутствии модификаций. «Все
           марки» / «Все модели» и очистка поля означают, что условие не задано. При смене марки
           список моделей перестраивается, а модель, которой нет в новом списке, сбрасывается.
           Правки маски, внесённые вручную в открытом окне, не сохраняются между открытиями —
           при следующем открытии маска снова формируется из АИС (правило 3). Изменение условий
           (ручное или через переоткрытие) не сбрасывает уже отобранные модификации (правило 3а).
        5. **Нормы.** По текущей группе сразу считаются минимум $H_{min}$, максимум $H_{max}$ и
           базовая норма $H_s$ (секция методики `fuel.base_norm`): $H_s = (H_{max} + H_{min}) / 2$.
           $H_s$ показывается в поле «Базовая норма расхода», $H_{min}$ и $H_{max}$ используются
           расчётом. Отдельно, независимо от «Отобранных», считается эталонная норма $H_{эт}$ —
           определение и формула — секция `fuel.reference_norm`
           {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Backend.Calc.DOC_NtsCalc>()}}. В отличие от
           $H_{min}$/$H_{max}$/$H_s$, $H_{эт}$ не пересчитывается при изменении отбора — только
           при пересборке самой базовой группы (правило 2); меняется на переоткрытии окна, если
           изменились вид ТС или вид топлива ТС.
        6. **Статус подбора** — свойство расчёта ТС
           ({{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Domain.DOC_VehicleCalc>()}}): «Не определена» —
           ни одной отобранной модификации; «Определена» — одна; «Определена группа» — несколько.
           Бейдж «Не определена» в карточке ТС — красный (не нейтральный): подбор уже проводился
           и дал пустой результат, это не то же самое, что «подбор ещё не выполнялся».
        7. **Способ подбора** — свойство расчёта ТС: «не определён» — подбор ещё не выполнялся;
           «авто» — группа получена сервисом при загрузке ТС и пользователь не нажимал
           «Применить»; «эксперт» — пользователь нажал «Применить».
        8. **«Применить»** сохраняет группу (содержимое «Отобранных») в
           {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Domain.DOC_SelectedModifications>()}}; маску
           не сохраняет — она не хранится нигде (правило 3). Ставит
           способ «эксперт», пересчитывает статус и $H_{min}$, $H_{max}$, $H_s$. Марка, модель и
           модификация записываются в
           {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Domain.DOC_Vehicle>()}} только если они
           однозначны: при одной отобранной — все три; при нескольких модификациях модификация не
           записывается, при нескольких моделях — и модель, при нескольких марках — и марка.
           «Применить» — самостоятельный триггер расчёта, независимый от приёма данных из АИС
           ({{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Backend.Calc.DOC_AisRefreshPolicy>()}}
           описывает только триггеры, связанные с АИС): расчёт запускается безусловно тем же
           механизмом, что и всегда — валидаторы секций
           ({{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Backend.Calc.DOC_CalcValidators>()}}) либо
           дают результат, либо классифицированную ошибку; само применение модификации отдельных
           условий на запуск расчёта не накладывает.
        9. **Пустая группа.** Если в отобранных нет ни одной модификации с установленной нормой,
           статус — «Не определена», $H_s$ не определена, «Применить» недоступна. Расчёт всё равно
           запускается безусловно (правило 8) — валидатор секции `fuel.base_norm` возвращает
           «Базовая норма не определена», статус расчёта «Выполнен с ошибкой», статус обработки
           «Нет модификаций» (см. {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Ui.DOC_VehicleCard>()}},
           {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Backend.Calc.DOC_ProcessingStatusPolicy>()}},
           правило 2).
        10. **Создание недостающей модификации.** Кнопка «Создать модификацию» в подвале окна
           доступна всегда, не только при пустом отборе: нужной записи может не быть в справочнике
           и тогда, когда отбор по виду ТС и топливу непуст (например, нет нужного сочетания
           марки/модели/модификации) — по правам пользователя (роль «Администратор модуля»,
           {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Vision.DOC_NtsVision>()}}, «Роли
           пользователей»). Кнопка открывает
           {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Ui.DOC_VehicleModificationCard>()}} с видом ТС
           и видом топлива, перенесёнными точно, и маркой/моделью — предложением, разобранным из
           «Марка и модель базы ТС» отдельной композицией распознавания
           ({{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Backend.ModificationSelection.DOC_ModificationNameRecognition>()}}):
           марка — распознанная марка, модель — остаток наименования без марки. Сохранение не
           закрывает окно подбора и не применяет группу автоматически: новая запись становится
           доступна для отбора в этом же окне, а условия подбора пересчитываются
           ({{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Epics.NtsFuelCalc.UseCases.UC_CreateVehicleModification>()}}).

        ### Поведение

        - При открытии выводятся контекст ТС (наименование, вид ТС, вид топлива); маска
          формируется из АИС (правило 3), «Отфильтрованные» строятся по ней, «Отобранные» разово
          заполняются текущей группой (правило 1). Состав «Отфильтрованных» строится в порядке:
          Вид ТС → Вид топлива → Марка → Модель → Модификация.
        - Если для вида ТС и вида топлива модификаций с установленной нормой нет, блок
          «Отфильтрованные модификации» показывает «нет модификаций для Вид ТС «…» и Вид топлива
          «…»»; итоговая строка — «—», «Применить» недоступна (если и «Отобранные» при этом пусты).
          Кнопка «Создать модификацию» в подвале окна при этом доступна, как и всегда
          (администратору модуля). Тот же текст показывается, когда модификации есть, но их
          отсекли ручные условия маски.
        - Изменение Марки, Модели или Модификации (как при переоткрытии, так и вручную) пересчитывает
          «Отфильтрованные»; «Отобранные» не меняются (правило 3а).
        - Список марок — все марки строк справочника по виду ТС. Список моделей ограничен
          выбранной маркой и перестраивается при её смене; модель, которой нет в новом списке,
          сбрасывается.
        - Действия с чекбоксами и блокировкой строк — правило 3б.
        - При изменении отобранных пересчитываются $H_{min}$, $H_{max}$ и $H_s$ и обновляется
          баннер: зелёный — в отобранных одна модификация, жёлтый — несколько, серый — ни одной.
        - Итоговая строка выводится в верхней части окна, над всеми блоками, и выделена как
          результат работы окна: `H_min: X · H_max: Y · Hs: Z л/100 км`; при отсутствии норм — `—`.
        - «Применить» недоступна, если в отобранных нет модификаций с установленной нормой.
        - «Отменить» — окно закрывается без изменений.
        - «Обновить из АИС»: маска и «Отфильтрованные» в любом случае пересобираются заново из
          текущих данных АИС при следующем открытии окна (правило 3) — отдельного поведения для
          «Обновить из АИС» по маске не требуется. Влияние смены вида ТС или вида топлива на уже
          сохранённые «Отобранные» описано в {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Backend.Calc.DOC_AisRefreshPolicy>()}}
          (раздел «Подбор модификации», строка «Изменились»): прежняя группа отбрасывается
          целиком. Сами триггеры обновления —
          {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Backend.Calc.DOC_AisRefreshPolicy>()}}.

        ### Открытые вопросы

        Открытые вопросы окна подбора модификаций — мастер-список марок и алиасы, источник для
        спецтехники и различение пустых состояний — собраны в эпике:
        {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Epics.NtsFuelCalc.OQ_ModificationSelection>()}}.
        Статусная модель там же.
        """";
    }
}