| | | 1 | | @model UsersIndexVm |
| | | 2 | | @{ |
| | 0 | 3 | | var json = new System.Text.Json.JsonSerializerOptions(System.Text.Json.JsonSerializerDefaults.Web); |
| | | 4 | | } |
| | | 5 | | |
| | | 6 | | <section class="admin-hero"> |
| | | 7 | | <h1>Пользователи</h1> |
| | | 8 | | <p class="subtitle">Управление входом, ролями и доступом.</p> |
| | | 9 | | <p class="subtitle-help">Отображаются профиль, статусы блокировки и одобрения входа.</p> |
| | | 10 | | </section> |
| | | 11 | | |
| | | 12 | | <nav class="admin-quick-links" aria-label="Связанные разделы"> |
| | | 13 | | <a asp-area="Admin" asp-controller="Roles" asp-action="Index"><i class="bi bi-person-badge"></i>Роли</a> |
| | | 14 | | <a asp-area="Admin" asp-controller="DataScopes" asp-action="Index"><i class="bi bi-funnel"></i>Наборы данных</a> |
| | | 15 | | <a asp-area="Admin" asp-controller="AuditLog" asp-action="Index"><i class="bi bi-journal-text"></i>Аудит</a> |
| | | 16 | | </nav> |
| | | 17 | | |
| | | 18 | | <section class="admin-intro"> |
| | | 19 | | <i class="bi bi-info-circle"></i> |
| | | 20 | | <div>Статус «Доступ» вычисляется по двум признакам: подтверждение email и одобрение администратором. Двойной клик по |
| | | 21 | | </section> |
| | | 22 | | |
| | | 23 | | <div class="admin-toolbar admin-surface"> |
| | | 24 | | <div class="bsx-dir-stats"> |
| | 0 | 25 | | <span class="bsx-dir-stat">Всего <b>@Model.Items.Count</b></span> |
| | 0 | 26 | | <span class="bsx-dir-stat bsx-dir-stat--warn">Не одобрены <b>@Model.Items.Count(u => !(u.EmailConfirmed && u.IsA |
| | 0 | 27 | | <span class="bsx-dir-stat">Заблокированы <b>@Model.Items.Count(u => u.IsLocked)</b></span> |
| | | 28 | | </div> |
| | 0 | 29 | | <a asp-action="Create" asp-route-q="@Model.Query" class="btn btn-primary btn-sm"><i class="bi bi-person-plus"></i> С |
| | | 30 | | </div> |
| | | 31 | | |
| | 0 | 32 | | @await Html.PartialAsync("~/Areas/Studio2/Views/Shared/_BsxDirectoryGrid.cshtml", new BsxDirectoryGridViewModel("admUser |
| | 0 | 33 | | <script type="application/json" id="admUsersData">@Html.Raw(System.Text.Json.JsonSerializer.Serialize(AdminGridRows.User |
| | | 34 | | |
| | | 35 | | @section Scripts { |
| | | 36 | | <script> |
| | | 37 | | (function () { |
| | | 38 | | const A = window.AdminGrid, D = window.BsxDirectory, esc = A.esc; |
| | 0 | 39 | | const editUrl = id => '@Url.Action("Edit")/' + encodeURIComponent(id); |
| | 0 | 40 | | const lockUrl = id => '@Url.Action("Lock")/' + encodeURIComponent(id); |
| | 0 | 41 | | const unlockUrl = id => '@Url.Action("Unlock")/' + encodeURIComponent(id); |
| | | 42 | | const grid = A.grid({ |
| | | 43 | | key: 'admUsers', prefix: 'admUsers', rows: A.data('admUsersData'), |
| | | 44 | | rowUrl: r => editUrl(r.id), |
| | | 45 | | columnDefs: [ |
| | | 46 | | { |
| | | 47 | | colId: 'user', field: 'displayName', headerName: 'Пользователь', minWidth: 220, flex: 2, |
| | | 48 | | getQuickFilterText: p => [p.data.displayName, p.data.secondaryName].join(' '), |
| | | 49 | | cellRenderer: p => { |
| | | 50 | | const r = p.data; if (!r) return ''; |
| | | 51 | | const avatar = r.avatarUrl |
| | | 52 | | ? '<img src="' + esc(r.avatarUrl) + '" alt="" class="admin-user-avatar" referrerpolicy=" |
| | | 53 | | : '<span class="admin-user-avatar-fallback">' + esc(r.initials) + '</span>'; |
| | | 54 | | return '<span class="admin-user">' + avatar + '<span class="bsx-cell-stack"><b>' + esc(r.dis |
| | | 55 | | (r.secondaryName ? '<small class="bsx-muted">' + esc(r.secondaryName) + '</small>' : '') |
| | | 56 | | } |
| | | 57 | | }, |
| | | 58 | | { colId: 'email', field: 'email', headerName: 'Email', minWidth: 200, flex: 2 }, |
| | | 59 | | D.setCol({ |
| | | 60 | | colId: 'roles', headerName: 'Роли', minWidth: 200, flex: 2, autoHeight: true, |
| | | 61 | | valueGetter: p => p.data ? (p.data.roles || []).join(', ') : '', |
| | | 62 | | cellRenderer: p => { |
| | | 63 | | const roles = p.data ? p.data.roles || [] : []; |
| | | 64 | | return roles.length ? '<span class="admin-chips">' + roles.map(r => D.tone(r, 'accent')).joi |
| | | 65 | | } |
| | | 66 | | }), |
| | | 67 | | D.numCol({ colId: 'overrideCount', field: 'overrideCount', headerName: 'Override', width: 110, value |
| | | 68 | | D.setCol({ |
| | | 69 | | colId: 'isLocked', headerName: 'Блокировка', width: 140, |
| | | 70 | | valueGetter: p => p.data ? String(p.data.isLocked) : '', |
| | | 71 | | filterParams: { labels: { 'true': 'Заблокирован', 'false': 'Активен' } }, |
| | | 72 | | cellRenderer: p => p.value === 'true' ? D.tone('Заблокирован', 'danger') : D.tone('Активен', 'su |
| | | 73 | | }), |
| | | 74 | | D.setCol({ |
| | | 75 | | colId: 'isApproved', headerName: 'Доступ', width: 130, |
| | | 76 | | valueGetter: p => p.data ? String(p.data.isApproved) : '', |
| | | 77 | | filterParams: { labels: { 'true': 'Одобрен', 'false': 'Не одобрен' } }, |
| | | 78 | | cellRenderer: p => p.value === 'true' ? D.tone('Одобрен', 'success') : D.tone('Не одобрен', 'war |
| | | 79 | | }), |
| | | 80 | | D.numCol({ colId: 'accessFailedCount', field: 'accessFailedCount', headerName: 'Вход (ошибок)', widt |
| | | 81 | | D.actionsCol(r => [ |
| | | 82 | | { icon: 'fa-pen', title: 'Настроить', label: 'Настроить', kind: 'edit', href: editUrl(r.id) }, |
| | | 83 | | r.isLocked |
| | | 84 | | ? { icon: 'fa-lock-open', title: 'Снять блокировку и сбросить неудачные попытки входа', kind |
| | | 85 | | onClick: row => A.confirmPost(unlockUrl(row.id), {}, { title: 'Разблокировать пользовате |
| | | 86 | | : { icon: 'fa-lock', title: 'Заблокировать вход', kind: 'danger', |
| | | 87 | | onClick: row => A.confirmPost(lockUrl(row.id), {}, { title: 'Заблокировать пользователя' |
| | | 88 | | ], { width: 170 }) |
| | | 89 | | ] |
| | | 90 | | }); |
| | | 91 | | })(); |
| | | 92 | | </script> |
| | | 93 | | } |