ui: refine admin workspace
This commit is contained in:
parent
f4a162a485
commit
d3f8ea3006
|
|
@ -18,43 +18,89 @@
|
|||
min-height: 0;
|
||||
/* unico contenitore scrollabile della console admin:
|
||||
header + sidebar + contenuto scorrono insieme */
|
||||
gap: 14px;
|
||||
padding: 16px clamp(14px, 2.5vw, 32px) 24px;
|
||||
background: #f8fafc;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.admin-shell__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 6px 4px 10px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.admin-shell__brand-mark {
|
||||
display: grid;
|
||||
flex: 0 0 auto;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
place-items: center;
|
||||
border-radius: 10px;
|
||||
background: #eff6ff;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.admin-shell__brand-mark mat-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.admin-shell__header-copy {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.admin-shell__eyebrow {
|
||||
color: #2563eb;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.admin-shell__title {
|
||||
margin: 4px 0 0;
|
||||
margin: 2px 0 0;
|
||||
color: #0f172a;
|
||||
font-size: 26px;
|
||||
font-weight: 800;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.admin-shell__subtitle {
|
||||
margin: 6px 0 0;
|
||||
margin: 3px 0 0;
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.admin-shell__header-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex: 0 0 auto;
|
||||
padding: 6px 9px;
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.76);
|
||||
color: #1d4ed8;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.admin-shell__header-status mat-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.admin-shell__body {
|
||||
display: grid;
|
||||
grid-template-columns: 240px minmax(0, 1fr);
|
||||
gap: 18px;
|
||||
grid-template-columns: 224px minmax(0, 1fr);
|
||||
gap: 14px;
|
||||
flex: 1 0 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
|
@ -62,23 +108,26 @@
|
|||
.admin-shell__sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
padding: 18px 14px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||
border-radius: 22px;
|
||||
background: linear-gradient(180deg, #f8fbff 0%, #eef5ff 100%);
|
||||
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.06);
|
||||
gap: 12px;
|
||||
padding: 12px 10px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 14px;
|
||||
background: #fff;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.admin-shell__menu-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.admin-shell__sidebar-title {
|
||||
color: #0f172a;
|
||||
font-size: 12px;
|
||||
padding: 0 8px;
|
||||
color: #64748b;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
|
|
@ -94,14 +143,14 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.24);
|
||||
padding: 9px 10px;
|
||||
border-radius: 9px;
|
||||
border: 1px solid #e2e8f0;
|
||||
color: #0f172a;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
background: #fff;
|
||||
transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;
|
||||
}
|
||||
|
||||
|
|
@ -115,24 +164,34 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 16px;
|
||||
padding: 9px 10px;
|
||||
border-radius: 9px;
|
||||
color: #334155;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
transition: background-color 120ms ease, color 120ms ease, transform 120ms ease;
|
||||
position: relative;
|
||||
transition: background-color 120ms ease, color 120ms ease, transform 120ms ease, box-shadow 120ms ease;
|
||||
}
|
||||
|
||||
.admin-shell__nav-link:hover {
|
||||
background: rgba(37, 99, 235, 0.08);
|
||||
background: #eff6ff;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.admin-shell__nav-link--active {
|
||||
background: #eff6ff;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.admin-shell__nav-link--active::before {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 3px;
|
||||
height: 20px;
|
||||
border-radius: 999px;
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
box-shadow: 0 12px 24px rgba(37, 99, 235, 0.2);
|
||||
content: '';
|
||||
}
|
||||
|
||||
.admin-shell__content {
|
||||
|
|
@ -147,10 +206,15 @@
|
|||
|
||||
.admin-shell__sidebar {
|
||||
padding: 14px;
|
||||
position: static;
|
||||
}
|
||||
|
||||
.admin-shell__nav {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.admin-shell__header-status {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,10 +6,17 @@
|
|||
|
||||
<div class="admin-shell">
|
||||
<div class="admin-shell__header">
|
||||
<div>
|
||||
<div class="admin-shell__eyebrow">Administration</div>
|
||||
<div class="admin-shell__brand-mark" aria-hidden="true">
|
||||
<mat-icon fontIcon="admin_panel_settings"></mat-icon>
|
||||
</div>
|
||||
<div class="admin-shell__header-copy">
|
||||
<div class="admin-shell__eyebrow">Administration workspace</div>
|
||||
<h1 class="admin-shell__title">Admin Console</h1>
|
||||
<p class="admin-shell__subtitle">Manage users and admin-only operations from a dedicated workspace.</p>
|
||||
<p class="admin-shell__subtitle">Users, access and service health in one focused workspace.</p>
|
||||
</div>
|
||||
<div class="admin-shell__header-status">
|
||||
<mat-icon fontIcon="verified_user"></mat-icon>
|
||||
Restricted access
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -7,34 +7,37 @@
|
|||
.admin-activity-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
padding-block: 8px;
|
||||
gap: 14px;
|
||||
padding: 8px 4px 16px;
|
||||
}
|
||||
|
||||
.admin-activity-page__header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.admin-activity-page__eyebrow {
|
||||
font-size: 12px;
|
||||
color: #2563eb;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.admin-activity-page__title {
|
||||
margin: 4px 0 6px;
|
||||
font-size: 22px;
|
||||
margin: 2px 0 4px;
|
||||
color: #0f172a;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.admin-activity-page__subtitle {
|
||||
margin: 0;
|
||||
max-width: 62ch;
|
||||
opacity: 0.75;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.admin-activity-page__actions {
|
||||
|
|
@ -53,10 +56,11 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 10px;
|
||||
padding: 9px 11px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid transparent;
|
||||
font-weight: 500;
|
||||
box-shadow: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.admin-activity-verdict--clear {
|
||||
|
|
@ -77,11 +81,14 @@
|
|||
.admin-activity-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
|
||||
gap: 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.admin-activity-card {
|
||||
padding: 16px;
|
||||
padding: 14px;
|
||||
border: 1px solid rgba(203, 213, 225, 0.84);
|
||||
border-radius: 14px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.admin-activity-card__title {
|
||||
|
|
@ -94,10 +101,18 @@
|
|||
}
|
||||
|
||||
.admin-activity-card__count {
|
||||
font-size: 24px;
|
||||
color: #1d4ed8;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
:host ::ng-deep .mat-mdc-button-base {
|
||||
min-height: 34px;
|
||||
padding-inline: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.admin-activity-card__hint {
|
||||
margin: 6px 0 12px;
|
||||
font-size: 12px;
|
||||
|
|
|
|||
|
|
@ -7,37 +7,36 @@
|
|||
.admin-create-user-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
gap: 14px;
|
||||
min-height: 100%;
|
||||
padding: 8px 4px 16px;
|
||||
background: linear-gradient(180deg, #f8fafc 0%, #eef6ff 100%);
|
||||
}
|
||||
|
||||
.admin-create-user-page__eyebrow {
|
||||
color: #2563eb;
|
||||
font-size: 12px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.admin-create-user-page__title {
|
||||
margin: 4px 0 0;
|
||||
margin: 2px 0 0;
|
||||
color: #0f172a;
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.admin-create-user-page__subtitle {
|
||||
margin: 6px 0 0;
|
||||
margin: 3px 0 0;
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.admin-create-user-page__success,
|
||||
.admin-create-user-card__error {
|
||||
border-radius: 14px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 10px;
|
||||
padding: 9px 11px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
|
@ -49,15 +48,17 @@
|
|||
}
|
||||
|
||||
.admin-create-user-card {
|
||||
border-radius: 22px;
|
||||
padding: 20px;
|
||||
border-radius: 14px;
|
||||
padding: 14px;
|
||||
max-width: 980px;
|
||||
border: 1px solid #e2e8f0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.admin-create-user-card__title {
|
||||
margin-bottom: 14px;
|
||||
color: #0f172a;
|
||||
font-size: 18px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
|
|
@ -87,6 +88,16 @@
|
|||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
:host ::ng-deep .mat-mdc-button-base {
|
||||
min-height: 34px;
|
||||
padding-inline: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.admin-create-user-check {
|
||||
|
|
@ -103,7 +114,9 @@
|
|||
|
||||
.admin-create-user-form__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
|
|
|
|||
|
|
@ -7,42 +7,43 @@
|
|||
.admin-stats-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
gap: 14px;
|
||||
min-height: 100%;
|
||||
padding: 8px 4px 16px;
|
||||
background: linear-gradient(180deg, #f8fafc 0%, #eef6ff 100%);
|
||||
}
|
||||
|
||||
.admin-stats-page__eyebrow {
|
||||
color: #2563eb;
|
||||
font-size: 12px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.admin-stats-page__title {
|
||||
margin: 4px 0 0;
|
||||
margin: 2px 0 0;
|
||||
color: #0f172a;
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.admin-stats-page__subtitle {
|
||||
margin: 6px 0 0;
|
||||
margin: 3px 0 0;
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.admin-stats-card {
|
||||
border-radius: 22px;
|
||||
padding: 20px;
|
||||
border-radius: 14px;
|
||||
padding: 14px;
|
||||
border: 1px solid #e2e8f0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.admin-stats-card__title {
|
||||
margin-bottom: 14px;
|
||||
color: #0f172a;
|
||||
font-size: 18px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
|
|
@ -69,7 +70,7 @@
|
|||
.admin-stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.admin-stats-users {
|
||||
|
|
@ -84,20 +85,19 @@
|
|||
flex-direction: column;
|
||||
gap: 3px;
|
||||
min-width: 180px;
|
||||
padding: 12px 14px;
|
||||
padding: 9px 11px;
|
||||
border: 1px solid #dbe7f5;
|
||||
border-radius: 16px;
|
||||
background: #f8fbff;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
color: #334155;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: border-color 120ms ease, background-color 120ms ease, transform 120ms ease;
|
||||
transition: border-color 120ms ease, background-color 120ms ease;
|
||||
}
|
||||
|
||||
.admin-stats-user-chip:hover {
|
||||
border-color: #93c5fd;
|
||||
background: #eff6ff;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.admin-stats-user-chip--active {
|
||||
|
|
@ -117,8 +117,11 @@
|
|||
}
|
||||
|
||||
.admin-stats-metric {
|
||||
border-radius: 20px;
|
||||
padding: 18px;
|
||||
border-radius: 14px;
|
||||
padding: 14px;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: #fff;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.admin-stats-metric__label {
|
||||
|
|
@ -132,8 +135,14 @@
|
|||
.admin-stats-metric__value {
|
||||
margin-top: 10px;
|
||||
color: #0f172a;
|
||||
font-size: 34px;
|
||||
font-weight: 800;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
:host ::ng-deep .mat-mdc-button-base {
|
||||
min-height: 34px;
|
||||
padding-inline: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.admin-stats-metric__meta {
|
||||
|
|
|
|||
|
|
@ -7,10 +7,9 @@
|
|||
.admin-users-list-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
gap: 14px;
|
||||
min-height: 100%;
|
||||
padding: 8px 4px 16px;
|
||||
background: linear-gradient(180deg, #f8fafc 0%, #eef6ff 100%);
|
||||
}
|
||||
|
||||
.admin-users-list-page__header {
|
||||
|
|
@ -22,30 +21,30 @@
|
|||
|
||||
.admin-users-list-page__eyebrow {
|
||||
color: #2563eb;
|
||||
font-size: 12px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.admin-users-list-page__title {
|
||||
margin: 4px 0 0;
|
||||
margin: 2px 0 0;
|
||||
color: #0f172a;
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.admin-users-list-page__subtitle {
|
||||
margin: 6px 0 0;
|
||||
margin: 3px 0 0;
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.admin-users-list-page__success,
|
||||
.admin-users-list-card__error,
|
||||
.admin-users-list-row__error {
|
||||
border-radius: 14px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 10px;
|
||||
padding: 9px 11px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
|
@ -57,17 +56,38 @@
|
|||
}
|
||||
|
||||
.admin-users-list-card {
|
||||
border-radius: 22px;
|
||||
padding: 20px;
|
||||
border-radius: 14px;
|
||||
padding: 14px;
|
||||
border: 1px solid #e2e8f0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.admin-users-list-card__toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.admin-users-list-card__title {
|
||||
margin-bottom: 14px;
|
||||
color: #0f172a;
|
||||
font-size: 18px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.admin-users-list-card__summary {
|
||||
margin-top: 4px;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.admin-users-list-card__search {
|
||||
width: min(100%, 340px);
|
||||
margin: -10px 0;
|
||||
}
|
||||
|
||||
.admin-users-list-card__error,
|
||||
.admin-users-list-row__error {
|
||||
border: 1px solid #fecaca;
|
||||
|
|
@ -81,10 +101,21 @@
|
|||
padding: 12px 4px;
|
||||
}
|
||||
|
||||
.admin-users-list-state--empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
min-height: 90px;
|
||||
border: 1px dashed #cbd5e1;
|
||||
border-radius: 16px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.admin-users-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.admin-users-list-row {
|
||||
|
|
@ -93,32 +124,58 @@
|
|||
gap: 16px;
|
||||
align-items: center;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 18px;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
padding: 12px;
|
||||
transition: border-color 140ms ease, background-color 140ms ease;
|
||||
}
|
||||
|
||||
.admin-users-list-row:hover {
|
||||
border-color: #bfdbfe;
|
||||
background: #f8fbff;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.admin-users-list-row__identity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
gap: 11px;
|
||||
}
|
||||
|
||||
.admin-users-list-row__avatar {
|
||||
display: grid;
|
||||
flex: 0 0 auto;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
place-items: center;
|
||||
border-radius: 9px;
|
||||
background: #eff6ff;
|
||||
color: #1d4ed8;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.admin-users-list-row__identity-copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.admin-users-list-row__username {
|
||||
color: #0f172a;
|
||||
font-size: 15px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.admin-users-list-row__email {
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
margin-top: 4px;
|
||||
margin-top: 2px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.admin-users-list-row__role {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.admin-users-list-row__role-field {
|
||||
|
|
@ -133,6 +190,35 @@
|
|||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.admin-users-list-row__role-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 7px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 999px;
|
||||
background: #f8fafc;
|
||||
color: #475569;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
:host ::ng-deep .mat-mdc-button-base {
|
||||
min-height: 34px;
|
||||
padding-inline: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:host ::ng-deep .mat-mdc-button-base .mat-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.admin-users-list-row__role-badge--admin {
|
||||
border-color: #c4b5fd;
|
||||
background: #f5f3ff;
|
||||
color: #6d28d9;
|
||||
}
|
||||
|
||||
.admin-users-list-row__error {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
|
@ -145,4 +231,14 @@
|
|||
.admin-users-list-row__actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.admin-users-list-card__toolbar {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.admin-users-list-card__search {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@
|
|||
<h2 class="admin-users-list-page__title">Users</h2>
|
||||
<p class="admin-users-list-page__subtitle">Review existing users, change roles, reset passwords and remove accounts.</p>
|
||||
</div>
|
||||
<button type="button" mat-stroked-button (click)="loadUsers()">
|
||||
<button type="button" mat-stroked-button [disabled]="loading()" (click)="loadUsers()">
|
||||
<mat-icon fontIcon="refresh"></mat-icon>
|
||||
Refresh
|
||||
{{ loading() ? 'Refreshing…' : 'Refresh' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
@ -22,7 +22,24 @@
|
|||
}
|
||||
|
||||
<mat-card class="admin-users-list-card">
|
||||
<div class="admin-users-list-card__title">Users</div>
|
||||
<div class="admin-users-list-card__toolbar">
|
||||
<div>
|
||||
<div class="admin-users-list-card__title">Users</div>
|
||||
<div class="admin-users-list-card__summary">
|
||||
{{ filteredUsers().length }} of {{ users().length }} account{{ users().length === 1 ? '' : 's' }} shown
|
||||
</div>
|
||||
</div>
|
||||
<mat-form-field appearance="outline" class="admin-users-list-card__search">
|
||||
<mat-label>Find a user</mat-label>
|
||||
<mat-icon matPrefix fontIcon="search"></mat-icon>
|
||||
<input matInput [ngModel]="userSearch()" (ngModelChange)="userSearch.set($event)" placeholder="Username, email or role" />
|
||||
@if (userSearch()) {
|
||||
<button type="button" mat-icon-button matSuffix aria-label="Clear user search" (click)="userSearch.set('')">
|
||||
<mat-icon fontIcon="close"></mat-icon>
|
||||
</button>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
@if (loading()) {
|
||||
<div class="admin-users-list-state">Loading users...</div>
|
||||
|
|
@ -30,13 +47,22 @@
|
|||
<div class="admin-users-list-card__error">{{ pageError() }}</div>
|
||||
} @else if (!users().length) {
|
||||
<div class="admin-users-list-state">No users found.</div>
|
||||
} @else if (!filteredUsers().length) {
|
||||
<div class="admin-users-list-state admin-users-list-state--empty">
|
||||
<mat-icon fontIcon="person_search"></mat-icon>
|
||||
No users match “{{ userSearch() }}”.
|
||||
<button type="button" mat-button (click)="userSearch.set('')">Clear search</button>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="admin-users-list">
|
||||
@for (user of users(); track user.username) {
|
||||
@for (user of filteredUsers(); track user.username) {
|
||||
<div class="admin-users-list-row">
|
||||
<div class="admin-users-list-row__identity">
|
||||
<div class="admin-users-list-row__username">{{ user.username }}</div>
|
||||
<div class="admin-users-list-row__email">{{ user.email || 'No email' }}</div>
|
||||
<div class="admin-users-list-row__avatar" aria-hidden="true">{{ user.username.slice(0, 1).toUpperCase() }}</div>
|
||||
<div class="admin-users-list-row__identity-copy">
|
||||
<div class="admin-users-list-row__username">{{ user.username }}</div>
|
||||
<div class="admin-users-list-row__email">{{ user.email || 'No email provided' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-users-list-row__role">
|
||||
|
|
@ -47,15 +73,23 @@
|
|||
<mat-option value="ADMIN">ADMIN</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<button type="button" mat-stroked-button [disabled]="roleSavingByUser()[user.username] || roleDraft(user.username) === user.role" (click)="saveRole(user)">
|
||||
Save role
|
||||
<button type="button" mat-flat-button [disabled]="roleSavingByUser()[user.username] || roleDraft(user.username) === user.role" (click)="saveRole(user)">
|
||||
<mat-icon fontIcon="save"></mat-icon>
|
||||
{{ roleSavingByUser()[user.username] ? 'Saving…' : 'Save role' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="admin-users-list-row__actions">
|
||||
<button type="button" mat-stroked-button (click)="openResetPasswordDialog(user)">Reset password</button>
|
||||
<span class="admin-users-list-row__role-badge" [class.admin-users-list-row__role-badge--admin]="user.role === 'ADMIN'">
|
||||
{{ user.role }}
|
||||
</span>
|
||||
<button type="button" mat-stroked-button (click)="openResetPasswordDialog(user)">
|
||||
<mat-icon fontIcon="key"></mat-icon>
|
||||
Reset password
|
||||
</button>
|
||||
<button type="button" mat-stroked-button color="warn" [disabled]="deleteBusyByUser()[user.username]" (click)="deleteUser(user)">
|
||||
Delete
|
||||
<mat-icon fontIcon="delete_outline"></mat-icon>
|
||||
{{ deleteBusyByUser()[user.username] ? 'Deleting…' : 'Delete' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -3,12 +3,13 @@
|
|||
// Attribution term under AGPL-3.0 section 7(b): see LICENSE-ADDENDUM.
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { AdminUser, UserRole } from '@models/user';
|
||||
import { Router } from '@angular/router';
|
||||
|
|
@ -27,6 +28,7 @@ import { scheduleSignalClear } from '@utilities/temporary-signal';
|
|||
MatCardModule,
|
||||
MatFormFieldModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
AdminResetPasswordDialogComponent
|
||||
],
|
||||
|
|
@ -40,6 +42,14 @@ export class AdminUsersListPage {
|
|||
private router = inject(Router);
|
||||
|
||||
readonly users = signal<AdminUser[]>([]);
|
||||
readonly userSearch = signal('');
|
||||
readonly filteredUsers = computed(() => {
|
||||
const query = this.userSearch().trim().toLowerCase();
|
||||
if (!query) return this.users();
|
||||
return this.users().filter((user) =>
|
||||
`${user.username} ${user.email ?? ''} ${user.role}`.toLowerCase().includes(query)
|
||||
);
|
||||
});
|
||||
readonly loading = signal(true);
|
||||
readonly pageError = signal<string | null>(null);
|
||||
readonly roleSavingByUser = signal<Record<string, boolean>>({});
|
||||
|
|
|
|||
Loading…
Reference in New Issue