< Summary

Information
Class: AspNetCoreGeneratedDocument.Areas_Admin_Views_Users_Edit
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Admin/Views/Users/Edit.cshtml
Line coverage
0%
Covered lines: 0
Uncovered lines: 39
Coverable lines: 39
Total lines: 314
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 74
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
ExecuteAsync()0%1806420%
<ExecuteAsync()0%4260%
<ExecuteAsync()100%210%
<ExecuteAsync()100%210%
<ExecuteAsync()0%210140%
<ExecuteAsync()0%2040%
<ExecuteAsync()0%4260%
<ExecuteAsync()0%620%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Admin/Views/Users/Edit.cshtml

#LineLine coverage
 1@model UserEditVm
 2
 3<section class="admin-hero">
 4    <h1>Карточка пользователя</h1>
 5    <p class="subtitle">Настройка ролей, статусов входа и персональных ограничений.</p>
 6    <p class="subtitle-help">Одобрение администратором автоматически подтверждает email.</p>
 7</section>
 8
 9<nav class="admin-quick-links">
 10    <a asp-area="Admin" asp-controller="Users" asp-action="Index"><i class="bi bi-people"></i>К пользователям</a>
 11    <a asp-area="Admin" asp-controller="Roles" asp-action="Index"><i class="bi bi-person-badge"></i>Роли</a>
 12    <a asp-area="Admin" asp-controller="DataScopes" asp-action="Index"><i class="bi bi-funnel"></i>Наборы данных</a>
 13</nav>
 14
 15<div class="admin-help-grid">
 16    <article class="admin-help-card">
 17        <h6><i class="bi bi-google me-1"></i>Вход через Google</h6>
 18        <p>При каждом успешном входе через Google профиль (имя и аватар) автоматически обновляется. Доступ даётся только
 19    </article>
 20    <article class="admin-help-card">
 21        <h6><i class="bi bi-envelope-check me-1"></i>Email и одобрение</h6>
 22        <p>Смена email сбрасывает подтверждение и одобрение. Кнопка «Одобрить вход» снова включает доступ и подтверждает
 23    </article>
 24    <article class="admin-help-card">
 25        <h6><i class="bi bi-key me-1"></i>Смена пароля</h6>
 26        <p>Новый пароль применяется сразу. Пользователь должен использовать его при следующем входе (вход через Google п
 27    </article>
 28    <article class="admin-help-card">
 29        <h6><i class="bi bi-person-badge me-1"></i>Как работают роли</h6>
 30        <p>Роли дают доступ к страницам, колонкам и наборам данных. Персональные override у пользователя дополняют, а не
 31    </article>
 32</div>
 33
 34<div class="card mb-3">
 35    <div class="card-body">
 36        <div class="d-flex justify-content-between align-items-center">
 37            <div>
 38                <div class="text-muted small">Пользователь</div>
 039                <h5 class="mb-0">@Model.UserName <span class="text-muted small">@Model.Email</span></h5>
 40            </div>
 41            <a asp-action="Index" class="btn btn-outline-secondary"><i class="bi bi-arrow-left"></i> К списку</a>
 42        </div>
 43    </div>
 44</div>
 45
 46<div class="card mb-3">
 47    <div class="card-body">
 48        <div class="d-flex justify-content-between align-items-center mb-2">
 49            <h6 class="mb-0"><i class="bi bi-envelope-check"></i> Доступ и подтверждения</h6>
 50            <span class="text-muted small">Одобрение администратором включает подтверждение email</span>
 51        </div>
 52
 53        <div class="row g-3">
 54            <div class="col-12 col-lg-6">
 55                <div class="border rounded p-3 h-100">
 56                    <div class="small text-muted mb-2">Email пользователя</div>
 057                    <div class="mb-2"><strong>@Model.Email</strong></div>
 58                    <div class="mb-3">
 059                        @if (Model.EmailConfirmed)
 60                        {
 61                            <span class="bsx-tone bsx-tone--success">Email подтверждён</span>
 62                        }
 63                        else
 64                        {
 65                            <span class="bsx-tone bsx-tone--warning">Email не подтверждён</span>
 66                        }
 67                    </div>
 68
 069                    <form asp-action="ChangeEmail" asp-route-id="@Model.Id" method="post" class="mb-2">
 070                        @Html.AntiForgeryToken()
 71                        <label asp-for="EmailChange.NewEmail" class="form-label"></label>
 72                        <div class="input-group">
 73                            <input asp-for="EmailChange.NewEmail" class="form-control" />
 74                            <button type="submit" class="btn btn-outline-primary">
 75                                <i class="bi bi-pencil-square"></i> Изменить email
 76                            </button>
 77                        </div>
 78                        <span asp-validation-for="EmailChange.NewEmail" class="text-danger small"></span>
 79                    </form>
 80
 81                    <div class="small text-muted">
 82                        Отправка письма не требуется, если администратор нажимает «Одобрить вход».
 83                    </div>
 84                </div>
 85            </div>
 86
 87            <div class="col-12 col-lg-6">
 88                <div class="border rounded p-3 h-100">
 89                    <div class="small text-muted mb-2">Одобрение администратором</div>
 90                    <div class="mb-2">
 091                        @if (Model.IsAdminApproved)
 92                        {
 93                            <span class="bsx-tone bsx-tone--success">Одобрено админом</span>
 94                        }
 95                        else
 96                        {
 97                            <span class="bsx-tone bsx-tone--danger">Не одобрено</span>
 98                        }
 99                    </div>
 100                    <div class="small text-muted mb-3">
 0101                        @if (Model.AdminApprovedAtUtc.HasValue)
 102                        {
 0103                            <span>Последнее одобрение: @Model.AdminApprovedAtUtc.Value.ToLocalTime().ToString("g") (@Mod
 104                        }
 105                        else
 106                        {
 107                            <span>Одобрение ещё не выполнялось</span>
 108                        }
 109                    </div>
 110
 111                    <div class="d-flex gap-2">
 0112                        <form asp-action="ApproveSignIn" asp-route-id="@Model.Id" method="post">
 0113                            @Html.AntiForgeryToken()
 114                            <button type="submit" class="btn btn-success">
 115                                <i class="bi bi-check2-circle"></i> Одобрить вход
 116                            </button>
 117                        </form>
 0118                        <form asp-action="RevokeSignInApproval" asp-route-id="@Model.Id" method="post"
 119                              data-confirm-title="Снять одобрение входа"
 120                              data-confirm-message="Пользователь потеряет доступ до повторного одобрения. Продолжить?"
 121                              data-confirm-button-text="Снять одобрение"
 122                              data-confirm-button-class="btn btn-danger">
 0123                            @Html.AntiForgeryToken()
 124                            <button type="submit" class="btn btn-outline-danger">
 125                                <i class="bi bi-x-circle"></i> Снять одобрение
 126                            </button>
 127                        </form>
 128                    </div>
 129                </div>
 130            </div>
 131        </div>
 132    </div>
 133</div>
 134
 135<div class="card mb-3">
 136    <div class="card-body">
 137        <div class="d-flex justify-content-between align-items-center mb-2">
 138            <h6 class="mb-0"><i class="bi bi-key"></i> Смена пароля</h6>
 0139            <span class="text-muted small">Для пользователя: <strong>@Model.UserName</strong></span>
 140        </div>
 0141        <form asp-action="ChangePassword" asp-route-id="@Model.Id" method="post" novalidate>
 0142            @Html.AntiForgeryToken()
 143            <div asp-validation-summary="ModelOnly" class="alert alert-danger py-2 mb-3"></div>
 144            <div class="row g-3">
 145                <div class="col-12 col-md-4">
 146                    <label asp-for="PasswordChange.NewPassword" class="form-label"></label>
 147                    <input asp-for="PasswordChange.NewPassword" class="form-control" autocomplete="new-password" />
 148                    <span asp-validation-for="PasswordChange.NewPassword" class="text-danger small"></span>
 149                </div>
 150                <div class="col-12 col-md-4">
 151                    <label asp-for="PasswordChange.ConfirmPassword" class="form-label"></label>
 152                    <input asp-for="PasswordChange.ConfirmPassword" class="form-control" autocomplete="new-password" />
 153                    <span asp-validation-for="PasswordChange.ConfirmPassword" class="text-danger small"></span>
 154                </div>
 155                <div class="col-12 col-md-4 d-flex align-items-end">
 156                    <button type="submit" class="btn btn-outline-warning w-100">
 157                        <i class="bi bi-shield-lock"></i> Обновить пароль
 158                    </button>
 159                </div>
 160            </div>
 161            <div class="form-text mt-2">Минимум 8 символов.</div>
 162        </form>
 163    </div>
 164</div>
 165
 166<ul class="nav nav-tabs" role="tablist">
 167    <li class="nav-item"><a class="nav-link active" data-bs-toggle="tab" href="#tab-roles">Роли</a></li>
 0168    <li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-pages">Override страниц (@Model.PageOverrid
 0169    <li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-fields">Override полей (@Model.FieldOverrid
 0170    <li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-scopes">Наборы данных (@Model.SelectedDataS
 171</ul>
 172<div class="tab-content card-body border border-top-0 admin-tab-content">
 173    <div class="tab-pane active" id="tab-roles">
 0174        <form asp-action="SaveRoles" asp-route-id="@Model.Id" method="post">
 0175            @Html.AntiForgeryToken()
 176            <div class="row g-2">
 0177                @foreach (var r in Model.AllRoles)
 178                {
 179                    <div class="col-md-4">
 180                        <div class="form-check">
 181                            <input type="checkbox" name="roleIds" value="@r.Id" id="role-@r.Id" class="form-check-input"
 182                            <label class="form-check-label" for="role-@r.Id">
 0183                                <strong>@r.DisplayName</strong>
 0184                                @if (r.IsSystem) { <span class="bsx-tone bsx-tone--muted ms-1">sys</span> }
 0185                                <div class="small text-muted">@r.Name</div>
 186                            </label>
 187                        </div>
 188                    </div>
 189                }
 190            </div>
 191            <div class="mt-3">
 192                <button type="submit" class="btn btn-primary"><i class="bi bi-save"></i> Сохранить роли</button>
 193            </div>
 194        </form>
 195    </div>
 196
 197    <div class="tab-pane" id="tab-pages">
 0198        @if (!Model.PageOverrides.Any())
 199        {
 200            <div class="alert alert-info">Персональные override на страницы отсутствуют. Нужный override можно задать по
 201        }
 202        else
 203        {
 0204            @await Html.PartialAsync("~/Areas/Studio2/Views/Shared/_BsxDirectoryGrid.cshtml", new BsxDirectoryGridViewMo
 205        }
 206    </div>
 207
 208    <div class="tab-pane" id="tab-fields">
 0209        @if (!Model.FieldOverrides.Any())
 210        {
 211            <div class="alert alert-info">Персональные override на поля отсутствуют.</div>
 212        }
 213        else
 214        {
 0215            @await Html.PartialAsync("~/Areas/Studio2/Views/Shared/_BsxDirectoryGrid.cshtml", new BsxDirectoryGridViewMo
 216        }
 217    </div>
 218
 219    <div class="tab-pane" id="tab-scopes">
 0220        <form asp-action="SaveDataScopes" asp-route-id="@Model.Id" method="post">
 0221            @Html.AntiForgeryToken()
 222
 223            <div class="alert alert-info small mb-3">
 224                <i class="bi bi-info-circle"></i>
 225                Персональные наборы применяются <strong>в дополнение</strong> к наборам от ролей (объединение по ИЛИ).
 226                Если у пользователя нет ни ролевых, ни персональных наборов по сущности — он видит все её данные.
 227                Пометка <span class="bsx-tone bsx-tone--muted">от роли</span> означает, что набор уже даётся ролью польз
 228            </div>
 229
 0230            @if (!Model.AllDataScopes.Any())
 231            {
 232                <div class="alert alert-warning">
 233                    Наборы данных ещё не созданы.
 234                    <a asp-controller="DataScopes" asp-action="Templates" class="alert-link">Создать из шаблона</a>.
 235                </div>
 236            }
 237            else
 238            {
 239                <div id="admUserScopesSelected" hidden></div>
 0240                @await Html.PartialAsync("~/Areas/Studio2/Views/Shared/_BsxDirectoryGrid.cshtml", new BsxDirectoryGridVi
 241            }
 242
 243            <div class="mt-3 d-flex gap-2">
 244                <button type="submit" class="btn btn-primary"><i class="bi bi-save"></i> Сохранить наборы <span class="m
 245                <a asp-controller="DataScopes" asp-action="Templates" class="btn btn-outline-primary">
 246                    <i class="bi bi-magic"></i> Создать новый набор
 247                </a>
 248            </div>
 249        </form>
 250    </div>
 251</div>
 252
 253@{
 0254    var json = new System.Text.Json.JsonSerializerOptions(System.Text.Json.JsonSerializerDefaults.Web);
 255}
 0256<script type="application/json" id="admUserPageOvData">@Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.PageOve
 0257<script type="application/json" id="admUserFieldOvData">@Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.FieldO
 0258<script type="application/json" id="admUserScopesData">@Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.AllData
 259
 260@section Scripts {
 261    <partial name="_ValidationScriptsPartial" />
 262    <script>
 263        (function () {
 264            const A = window.AdminGrid, D = window.BsxDirectory, esc = A.esc;
 0265            const removePage = '@Url.Action("RemovePageOverride", new { id = Model.Id })';
 0266            const removeField = '@Url.Action("RemoveFieldOverride", new { id = Model.Id })';
 267            A.grid({
 268                key: 'admUserPageOv', prefix: 'admUserPageOv', rows: A.data('admUserPageOvData'), autoHeight: true,
 269                columnDefs: [
 270                    { colId: 'displayName', field: 'displayName', headerName: 'Страница', minWidth: 240, flex: 2,
 271                      getQuickFilterText: p => p.data.displayName + ' ' + p.data.code,
 272                      cellRenderer: p => p.data ? '<span class="bsx-cell-stack"><b>' + esc(p.data.displayName) + '</b><s
 273                    A.tri({ colId: 'canView', field: 'canView', headerName: 'View' }),
 274                    A.tri({ colId: 'canCreate', field: 'canCreate', headerName: 'Create' }),
 275                    A.tri({ colId: 'canEdit', field: 'canEdit', headerName: 'Edit' }),
 276                    A.tri({ colId: 'canDelete', field: 'canDelete', headerName: 'Delete' }),
 277                    A.tri({ colId: 'canExport', field: 'canExport', headerName: 'Export' }),
 278                    D.actionsCol(r => [{ icon: 'fa-xmark', title: 'Удалить override', kind: 'danger', post: true, href: 
 279                ]
 280            });
 281            A.grid({
 282                key: 'admUserFieldOv', prefix: 'admUserFieldOv', rows: A.data('admUserFieldOvData'), autoHeight: true,
 283                columnDefs: [
 284                    D.setCol({ colId: 'entityName', field: 'entityName', headerName: 'Сущность', width: 150 }),
 285                    { colId: 'displayName', field: 'displayName', headerName: 'Поле', minWidth: 220, flex: 2,
 286                      cellRenderer: p => p.data ? esc(p.data.displayName) + ' <span class="admin-code">' + esc(p.data.fi
 287                    A.tri({ colId: 'canRead', field: 'canRead', headerName: 'Read' }),
 288                    A.tri({ colId: 'canWrite', field: 'canWrite', headerName: 'Write' }),
 289                    D.actionsCol(r => [{ icon: 'fa-xmark', title: 'Удалить override', kind: 'danger', post: true, href: 
 290                ]
 291            });
 292            const counter = document.getElementById('admUserScopesChosen');
 293            A.selectionGrid({
 294                key: 'admUserScopes', prefix: 'admUserScopes', rows: A.data('admUserScopesData'), autoHeight: true,
 0295                selected: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.SelectedDataScopeIds)),
 296                inputName: 'scopeIds', hiddenId: 'admUserScopesSelected',
 297                isSelectable: r => r.isActive,
 298                onChange: ids => { if (counter) counter.textContent = '(' + ids.length + ')'; },
 299                rowClassRules: { 'bsx-row-muted': p => !!(p.data && !p.data.isActive) },
 300                columnDefs: [
 301                    D.setCol({ colId: 'entityName', field: 'entityName', headerName: 'Сущность', width: 150, cellRendere
 302                    { colId: 'displayName', field: 'displayName', headerName: 'Название', minWidth: 200, flex: 1,
 303                      getQuickFilterText: p => [p.data.displayName, p.data.description].join(' '),
 304                      cellRenderer: p => p.data ? '<span class="bsx-cell-stack"><b>' + esc(p.data.displayName) + '</b>' 
 305                    A.code({ colId: 'code', field: 'code', headerName: 'Код' }),
 306                    { colId: 'filterExpression', field: 'filterExpression', headerName: 'Dynamic LINQ', minWidth: 240, f
 307                      cellRenderer: p => '<span class="admin-expr">' + esc(p.value || '') + '</span>' },
 308                    A.check({ colId: 'fromRole', field: 'fromRole', headerName: 'От роли' }),
 309                    A.check({ colId: 'isActive', field: 'isActive', headerName: 'Активен' })
 310                ]
 311            });
 312        })();
 313    </script>
 314}