| | | 1 | | @model OzonOrdersWeb.Areas.Admin.Models.DataScopeFromTemplateVm |
| | | 2 | | |
| | | 3 | | <section class="admin-hero"> |
| | | 4 | | <h1>Создание набора из шаблона</h1> |
| | | 5 | | <p class="subtitle">Соберите ограничение данных с генерацией выражения.</p> |
| | | 6 | | <p class="subtitle-help">Проверьте код и название перед сохранением.</p> |
| | | 7 | | </section> |
| | | 8 | | |
| | | 9 | | <nav class="admin-quick-links"> |
| | | 10 | | <a asp-area="Admin" asp-controller="DataScopes" asp-action="Templates"><i class="bi bi-magic"></i>К шаблонам</a> |
| | | 11 | | <a asp-area="Admin" asp-controller="DataScopes" asp-action="Index"><i class="bi bi-funnel"></i>Реестр наборов</a> |
| | | 12 | | <a asp-area="Admin" asp-controller="Roles" asp-action="Index"><i class="bi bi-person-badge"></i>Роли</a> |
| | | 13 | | </nav> |
| | | 14 | | |
| | | 15 | | <form asp-action="FromTemplate" method="post"> |
| | 0 | 16 | | @Html.AntiForgeryToken() |
| | | 17 | | <input type="hidden" asp-for="TemplateKey"/> |
| | | 18 | | <input type="hidden" asp-for="EntityName"/> |
| | | 19 | | <input type="hidden" asp-for="FkField"/> |
| | | 20 | | <input type="hidden" asp-for="TemplateDisplayName"/> |
| | | 21 | | |
| | | 22 | | <div class="card mb-3"> |
| | | 23 | | <div class="card-body d-flex align-items-start gap-3"> |
| | | 24 | | <div class="template-icon"> |
| | | 25 | | <i class="bi @(string.IsNullOrEmpty(Model.TemplateIcon) ? "bi-funnel" : Model.TemplateIcon)"></i> |
| | | 26 | | </div> |
| | | 27 | | <div class="flex-grow-1"> |
| | 0 | 28 | | <div class="fw-semibold">@Model.TemplateDisplayName</div> |
| | 0 | 29 | | <div class="text-muted small">@Model.TemplateDescription</div> |
| | | 30 | | <div class="mt-2"> |
| | 0 | 31 | | <span class="bsx-tone bsx-tone--muted">Сущность: <strong>@Model.EntityName</strong></span> |
| | 0 | 32 | | @if (Model.IsRangeTemplate) |
| | | 33 | | { |
| | | 34 | | <span class="bsx-tone bsx-tone--info">Диапазон</span> |
| | | 35 | | } |
| | 0 | 36 | | <span class="bsx-tone bsx-tone--muted">Поле: <strong>@Model.FkField</strong></span> |
| | | 37 | | </div> |
| | | 38 | | </div> |
| | | 39 | | <a asp-action="Templates" class="btn btn-sm btn-outline-secondary"><i class="bi bi-arrow-left"></i> К шаблон |
| | | 40 | | </div> |
| | | 41 | | </div> |
| | | 42 | | |
| | | 43 | | <div class="row g-3"> |
| | | 44 | | <div class="col-lg-5"> |
| | | 45 | | <div class="card h-100"> |
| | | 46 | | <div class="card-header py-2"><strong>Параметры набора</strong></div> |
| | | 47 | | <div class="card-body"> |
| | | 48 | | <div class="mb-3"> |
| | | 49 | | <label asp-for="Code" class="form-label"></label> |
| | | 50 | | <input asp-for="Code" class="form-control"/> |
| | | 51 | | <span asp-validation-for="Code" class="text-danger small"></span> |
| | | 52 | | </div> |
| | | 53 | | <div class="mb-3"> |
| | | 54 | | <label asp-for="DisplayName" class="form-label"></label> |
| | | 55 | | <input asp-for="DisplayName" class="form-control"/> |
| | | 56 | | <span asp-validation-for="DisplayName" class="text-danger small"></span> |
| | | 57 | | </div> |
| | | 58 | | <div class="mb-3"> |
| | | 59 | | <label asp-for="Description" class="form-label"></label> |
| | | 60 | | <textarea asp-for="Description" rows="2" class="form-control"></textarea> |
| | | 61 | | </div> |
| | | 62 | | <div class="form-check"> |
| | | 63 | | <input asp-for="IsActive" class="form-check-input"/> |
| | | 64 | | <label class="form-check-label" asp-for="IsActive"></label> |
| | | 65 | | </div> |
| | | 66 | | </div> |
| | | 67 | | </div> |
| | | 68 | | </div> |
| | | 69 | | |
| | | 70 | | <div class="col-lg-7"> |
| | | 71 | | <div class="card h-100"> |
| | 0 | 72 | | @if (Model.IsRangeTemplate) |
| | | 73 | | { |
| | | 74 | | <div class="card-header py-2"><strong>Диапазон значений</strong></div> |
| | | 75 | | <div class="card-body"> |
| | | 76 | | <p class="text-muted small">Можно указать одну границу: только мин., только макс. или обе.</p> |
| | | 77 | | <div class="row g-2"> |
| | | 78 | | <div class="col-sm-6"> |
| | | 79 | | <label asp-for="MinValue" class="form-label">Минимум (включительно)</label> |
| | | 80 | | <input asp-for="MinValue" class="form-control" type="text" inputmode="decimal" placehold |
| | | 81 | | <span asp-validation-for="MinValue" class="text-danger small"></span> |
| | | 82 | | </div> |
| | | 83 | | <div class="col-sm-6"> |
| | | 84 | | <label asp-for="MaxValue" class="form-label">Максимум (включительно)</label> |
| | | 85 | | <input asp-for="MaxValue" class="form-control" type="text" inputmode="decimal" placehold |
| | | 86 | | <span asp-validation-for="MaxValue" class="text-danger small"></span> |
| | | 87 | | </div> |
| | | 88 | | </div> |
| | | 89 | | </div> |
| | | 90 | | } |
| | | 91 | | else |
| | | 92 | | { |
| | | 93 | | <div class="card-header py-2 d-flex align-items-center gap-2"> |
| | | 94 | | <strong>Выберите значения</strong> |
| | | 95 | | <span class="bsx-tone bsx-tone--accent" id="selectedCount">0</span> |
| | | 96 | | <span asp-validation-for="SelectedIds" class="text-danger small ms-2"></span> |
| | | 97 | | </div> |
| | | 98 | | <div class="card-body p-2"> |
| | 0 | 99 | | @if (Model.Items.Count == 0) |
| | | 100 | | { |
| | | 101 | | <div class="text-center bsx-muted py-5">Справочник пуст</div> |
| | | 102 | | } |
| | | 103 | | else |
| | | 104 | | { |
| | | 105 | | <div id="admTemplateSelected" hidden></div> |
| | 0 | 106 | | @await Html.PartialAsync("~/Areas/Studio2/Views/Shared/_BsxDirectoryGrid.cshtml", new BsxDir |
| | 0 | 107 | | "Галка в заголовке отмечает все строки, видимые с учётом поиска и фильтров.")) |
| | | 108 | | } |
| | | 109 | | </div> |
| | | 110 | | } |
| | | 111 | | <div class="card-footer small text-muted"> |
| | | 112 | | Результирующее выражение: |
| | | 113 | | <code id="exprPreview" class="ms-1"></code> |
| | | 114 | | </div> |
| | | 115 | | </div> |
| | | 116 | | </div> |
| | | 117 | | </div> |
| | | 118 | | |
| | | 119 | | <div class="mt-3 d-flex gap-2"> |
| | | 120 | | <button type="submit" class="btn btn-primary"><i class="bi bi-save"></i> Создать набор</button> |
| | | 121 | | <a asp-action="Templates" class="btn btn-outline-secondary">Отмена</a> |
| | | 122 | | </div> |
| | | 123 | | </form> |
| | | 124 | | |
| | 0 | 125 | | <script type="application/json" id="admTemplateItemsData">@Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.Item |
| | | 126 | | |
| | | 127 | | @section Scripts { |
| | | 128 | | <partial name="_ValidationScriptsPartial"/> |
| | | 129 | | <script> |
| | | 130 | | (function() { |
| | 0 | 131 | | const isRange = @(Model.IsRangeTemplate ? "true" : "false"); |
| | 0 | 132 | | const fkField = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.FkField)); |
| | 0 | 133 | | const rangeIsDecimal = @(Model.RangeIsDecimal ? "true" : "false"); |
| | | 134 | | const preview = document.getElementById('exprPreview'); |
| | | 135 | | if (isRange) { |
| | | 136 | | const minEl = document.getElementById('rangeMin'); |
| | | 137 | | const maxEl = document.getElementById('rangeMax'); |
| | | 138 | | function numOrNull(s) { |
| | | 139 | | s = (s || '').trim().replace(',', '.'); |
| | | 140 | | if (s === '') return null; |
| | | 141 | | const n = Number(s); |
| | | 142 | | return Number.isFinite(n) ? n : null; |
| | | 143 | | } |
| | | 144 | | function updateRangePreview() { |
| | | 145 | | const a = minEl ? numOrNull(minEl.value) : null; |
| | | 146 | | const b = maxEl ? numOrNull(maxEl.value) : null; |
| | | 147 | | if (a == null && b == null) { preview.textContent = '—'; return; } |
| | | 148 | | const parts = [fkField + ' != null']; |
| | | 149 | | if (rangeIsDecimal) { |
| | | 150 | | if (a != null) parts.push(fkField + ' >= ' + a); |
| | | 151 | | if (b != null) parts.push(fkField + ' <= ' + b); |
| | | 152 | | } else { |
| | | 153 | | if (a != null) parts.push(fkField + ' >= ' + Math.trunc(a)); |
| | | 154 | | if (b != null) parts.push(fkField + ' <= ' + Math.trunc(b)); |
| | | 155 | | } |
| | | 156 | | preview.textContent = '(' + parts.join(' && ') + ')'; |
| | | 157 | | } |
| | | 158 | | minEl?.addEventListener('input', updateRangePreview); |
| | | 159 | | maxEl?.addEventListener('input', updateRangePreview); |
| | | 160 | | updateRangePreview(); |
| | | 161 | | return; |
| | | 162 | | } |
| | | 163 | | const countEl = document.getElementById('selectedCount'); |
| | | 164 | | function updatePreview(values) { |
| | | 165 | | countEl.textContent = values.length; |
| | | 166 | | if (values.length === 0) { preview.textContent = '—'; return; } |
| | | 167 | | const allNumeric = values.every(v => /^-?\d+$/.test(v)); |
| | | 168 | | const parts = values.map(v => allNumeric |
| | | 169 | | ? `${fkField} == ${v}` |
| | | 170 | | : `${fkField} == "${v.replace(/"/g, '\\"')}"`); |
| | | 171 | | preview.textContent = '(' + parts.join(' || ') + ')'; |
| | | 172 | | } |
| | | 173 | | const A = window.AdminGrid; |
| | | 174 | | A.selectionGrid({ |
| | | 175 | | key: 'admTemplateItems', prefix: 'admTemplateItems', |
| | | 176 | | rows: A.data('admTemplateItemsData'), |
| | 0 | 177 | | selected: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.SelectedIds ?? Array.Empty<string>() |
| | | 178 | | inputName: 'SelectedIds', hiddenId: 'admTemplateSelected', |
| | | 179 | | onChange: updatePreview, |
| | | 180 | | columnDefs: [ |
| | | 181 | | { colId: 'name', field: 'name', headerName: 'Значение', minWidth: 240, flex: 3 }, |
| | | 182 | | { colId: 'id', field: 'id', headerName: 'ID', width: 120, cellClass: 'bsx-code', valueFormatter: p = |
| | | 183 | | ] |
| | | 184 | | }); |
| | | 185 | | })(); |
| | | 186 | | </script> |
| | | 187 | | } |