feat(admin): users and permissions
Sprint 25.
New features/admin/users/ module, net-new /:lang/backoffice/users route +
Dashboard Quick Action.
- users: name, Telegram username, scope (marketplace vs office admin),
role (inline change), status (active/invited/suspended), last login
- 4 built-in roles (owner/admin/editor/viewer) with flat permission lists
- invitations: email + role + scope form, pending list + revoke (no email
actually sends - local record only)
- passwordless login confirmed already real (AdminAuthService Telegram QR,
docs/BACKEND.md item 1) - linked, not reimplemented
- per-user mock session list (device/IP/last-active, revoke) - flagged as
mock since the real AdminAuthService only ever tracks the current
browser's session
- per-user audit log dialog (role/status changes), same pattern as
Sprint 24's per-transaction audit, intentionally separate from the
system-wide log planned for Sprint 26
docs/ADMIN.md + docs/BACKEND.md (new item 14) updated.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 11:05:21 +04:00
|
|
|
<section class="admin-users-page">
|
|
|
|
|
<div class="card">
|
|
|
|
|
<h2>{{ 'adminUsers.title' | translate }}</h2>
|
refactor: marketplace release polish
Sprint 28, scoped to admin/* (user decision — full marketplace audit
declined in favor of a bounded pass over the 8 admin features from
Sprints 20-27).
- a11y: aria-label added to every bare <select> not already inside a
<label> across categories/products/orders/transactions/users/monitoring
- loading states: app-skeleton rows/cards added to list pages that
previously rendered blank during the initial fetch (categories, orders,
transactions, users, monitoring's event feed, analytics summary cards)
- admin-dashboard-card's custom shimmer CSS replaced with the shared
SkeletonComponent (same visual result, one less duplicated animation)
- bundle-size budget warning (~198kB over) confirmed pre-existing —
present at Sprint 20's first build before any admin/* code existed,
and new admin pages are all lazy-loaded — documented as out of scope
for this pass rather than chased
docs/ADMIN.md + docs/SPRINT-PLAN.md updated with the scope decision and
what was explicitly not done (Lighthouse, animations, SEO/sitemap,
storefront/editor a11y).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 00:14:46 +04:00
|
|
|
@if (facade.loading()) {
|
|
|
|
|
<div class="skeleton-rows">
|
|
|
|
|
@for (i of [1,2,3]; track i) { <app-skeleton shape="rect" height="40px" /> }
|
|
|
|
|
</div>
|
|
|
|
|
} @else if (facade.users().length === 0) {
|
|
|
|
|
<app-empty-state [title]="'adminUsers.emptyTitle' | translate" [description]="'adminUsers.emptyDescription' | translate" />
|
|
|
|
|
} @else {
|
feat(admin): users and permissions
Sprint 25.
New features/admin/users/ module, net-new /:lang/backoffice/users route +
Dashboard Quick Action.
- users: name, Telegram username, scope (marketplace vs office admin),
role (inline change), status (active/invited/suspended), last login
- 4 built-in roles (owner/admin/editor/viewer) with flat permission lists
- invitations: email + role + scope form, pending list + revoke (no email
actually sends - local record only)
- passwordless login confirmed already real (AdminAuthService Telegram QR,
docs/BACKEND.md item 1) - linked, not reimplemented
- per-user mock session list (device/IP/last-active, revoke) - flagged as
mock since the real AdminAuthService only ever tracks the current
browser's session
- per-user audit log dialog (role/status changes), same pattern as
Sprint 24's per-transaction audit, intentionally separate from the
system-wide log planned for Sprint 26
docs/ADMIN.md + docs/BACKEND.md (new item 14) updated.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 11:05:21 +04:00
|
|
|
<app-table>
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
2026-07-23 11:06:17 +04:00
|
|
|
<th scope="col">{{ 'adminUsers.name' | translate }}</th>
|
|
|
|
|
<th scope="col">{{ 'adminUsers.scope' | translate }}</th>
|
|
|
|
|
<th scope="col">{{ 'adminUsers.role' | translate }}</th>
|
|
|
|
|
<th scope="col">{{ 'backoffice.status' | translate }}</th>
|
|
|
|
|
<th scope="col">{{ 'adminUsers.lastLogin' | translate }}</th>
|
|
|
|
|
<th scope="col">{{ 'adminProducts.actions' | translate }}</th>
|
feat(admin): users and permissions
Sprint 25.
New features/admin/users/ module, net-new /:lang/backoffice/users route +
Dashboard Quick Action.
- users: name, Telegram username, scope (marketplace vs office admin),
role (inline change), status (active/invited/suspended), last login
- 4 built-in roles (owner/admin/editor/viewer) with flat permission lists
- invitations: email + role + scope form, pending list + revoke (no email
actually sends - local record only)
- passwordless login confirmed already real (AdminAuthService Telegram QR,
docs/BACKEND.md item 1) - linked, not reimplemented
- per-user mock session list (device/IP/last-active, revoke) - flagged as
mock since the real AdminAuthService only ever tracks the current
browser's session
- per-user audit log dialog (role/status changes), same pattern as
Sprint 24's per-transaction audit, intentionally separate from the
system-wide log planned for Sprint 26
docs/ADMIN.md + docs/BACKEND.md (new item 14) updated.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 11:05:21 +04:00
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
@for (user of facade.users(); track user.id) {
|
|
|
|
|
<tr>
|
|
|
|
|
<td>{{ user.name }}<br /><small>{{ user.telegramUsername }}</small></td>
|
|
|
|
|
<td>{{ ('adminUsers.scopeValue.' + user.scope) | translate }}</td>
|
|
|
|
|
<td>
|
refactor: marketplace release polish
Sprint 28, scoped to admin/* (user decision — full marketplace audit
declined in favor of a bounded pass over the 8 admin features from
Sprints 20-27).
- a11y: aria-label added to every bare <select> not already inside a
<label> across categories/products/orders/transactions/users/monitoring
- loading states: app-skeleton rows/cards added to list pages that
previously rendered blank during the initial fetch (categories, orders,
transactions, users, monitoring's event feed, analytics summary cards)
- admin-dashboard-card's custom shimmer CSS replaced with the shared
SkeletonComponent (same visual result, one less duplicated animation)
- bundle-size budget warning (~198kB over) confirmed pre-existing —
present at Sprint 20's first build before any admin/* code existed,
and new admin pages are all lazy-loaded — documented as out of scope
for this pass rather than chased
docs/ADMIN.md + docs/SPRINT-PLAN.md updated with the scope decision and
what was explicitly not done (Lighthouse, animations, SEO/sitemap,
storefront/editor a11y).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 00:14:46 +04:00
|
|
|
<select [attr.aria-label]="'adminUsers.role' | translate" [ngModel]="user.roleId" (ngModelChange)="facade.setRole(user.id, $event)">
|
feat(admin): users and permissions
Sprint 25.
New features/admin/users/ module, net-new /:lang/backoffice/users route +
Dashboard Quick Action.
- users: name, Telegram username, scope (marketplace vs office admin),
role (inline change), status (active/invited/suspended), last login
- 4 built-in roles (owner/admin/editor/viewer) with flat permission lists
- invitations: email + role + scope form, pending list + revoke (no email
actually sends - local record only)
- passwordless login confirmed already real (AdminAuthService Telegram QR,
docs/BACKEND.md item 1) - linked, not reimplemented
- per-user mock session list (device/IP/last-active, revoke) - flagged as
mock since the real AdminAuthService only ever tracks the current
browser's session
- per-user audit log dialog (role/status changes), same pattern as
Sprint 24's per-transaction audit, intentionally separate from the
system-wide log planned for Sprint 26
docs/ADMIN.md + docs/BACKEND.md (new item 14) updated.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 11:05:21 +04:00
|
|
|
@for (role of facade.roles(); track role.id) {
|
|
|
|
|
<option [value]="role.id">{{ role.name }}</option>
|
|
|
|
|
}
|
|
|
|
|
</select>
|
|
|
|
|
</td>
|
|
|
|
|
<td><app-badge [variant]="user.status === 'active' ? 'success' : user.status === 'suspended' ? 'danger' : 'neutral'">{{ ('adminUsers.statusValue.' + user.status) | translate }}</app-badge></td>
|
|
|
|
|
<td>{{ user.lastLoginAt ? (user.lastLoginAt | date:'short') : '—' }}</td>
|
|
|
|
|
<td class="actions">
|
|
|
|
|
<app-button variant="secondary" size="sm" (click)="toggleStatus(user.id, user.status)">{{ (user.status === 'suspended' ? 'adminUsers.reactivate' : 'adminUsers.suspend') | translate }}</app-button>
|
|
|
|
|
<app-button variant="secondary" size="sm" (click)="facade.openSessions(user)">{{ 'adminUsers.sessions' | translate }}</app-button>
|
|
|
|
|
<app-button variant="secondary" size="sm" (click)="facade.openAudit(user)">{{ 'adminTransactions.audit' | translate }}</app-button>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
}
|
|
|
|
|
</tbody>
|
|
|
|
|
</app-table>
|
refactor: marketplace release polish
Sprint 28, scoped to admin/* (user decision — full marketplace audit
declined in favor of a bounded pass over the 8 admin features from
Sprints 20-27).
- a11y: aria-label added to every bare <select> not already inside a
<label> across categories/products/orders/transactions/users/monitoring
- loading states: app-skeleton rows/cards added to list pages that
previously rendered blank during the initial fetch (categories, orders,
transactions, users, monitoring's event feed, analytics summary cards)
- admin-dashboard-card's custom shimmer CSS replaced with the shared
SkeletonComponent (same visual result, one less duplicated animation)
- bundle-size budget warning (~198kB over) confirmed pre-existing —
present at Sprint 20's first build before any admin/* code existed,
and new admin pages are all lazy-loaded — documented as out of scope
for this pass rather than chased
docs/ADMIN.md + docs/SPRINT-PLAN.md updated with the scope decision and
what was explicitly not done (Lighthouse, animations, SEO/sitemap,
storefront/editor a11y).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 00:14:46 +04:00
|
|
|
}
|
feat(admin): users and permissions
Sprint 25.
New features/admin/users/ module, net-new /:lang/backoffice/users route +
Dashboard Quick Action.
- users: name, Telegram username, scope (marketplace vs office admin),
role (inline change), status (active/invited/suspended), last login
- 4 built-in roles (owner/admin/editor/viewer) with flat permission lists
- invitations: email + role + scope form, pending list + revoke (no email
actually sends - local record only)
- passwordless login confirmed already real (AdminAuthService Telegram QR,
docs/BACKEND.md item 1) - linked, not reimplemented
- per-user mock session list (device/IP/last-active, revoke) - flagged as
mock since the real AdminAuthService only ever tracks the current
browser's session
- per-user audit log dialog (role/status changes), same pattern as
Sprint 24's per-transaction audit, intentionally separate from the
system-wide log planned for Sprint 26
docs/ADMIN.md + docs/BACKEND.md (new item 14) updated.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 11:05:21 +04:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
<h2>{{ 'adminUsers.invite' | translate }}</h2>
|
|
|
|
|
<p class="hint">{{ 'adminUsers.passwordlessHint' | translate }}</p>
|
|
|
|
|
<div class="invite-form">
|
|
|
|
|
<app-input type="email" [ngModel]="inviteEmail()" (ngModelChange)="inviteEmail.set($event)" [placeholder]="'adminUsers.email' | translate" />
|
refactor: marketplace release polish
Sprint 28, scoped to admin/* (user decision — full marketplace audit
declined in favor of a bounded pass over the 8 admin features from
Sprints 20-27).
- a11y: aria-label added to every bare <select> not already inside a
<label> across categories/products/orders/transactions/users/monitoring
- loading states: app-skeleton rows/cards added to list pages that
previously rendered blank during the initial fetch (categories, orders,
transactions, users, monitoring's event feed, analytics summary cards)
- admin-dashboard-card's custom shimmer CSS replaced with the shared
SkeletonComponent (same visual result, one less duplicated animation)
- bundle-size budget warning (~198kB over) confirmed pre-existing —
present at Sprint 20's first build before any admin/* code existed,
and new admin pages are all lazy-loaded — documented as out of scope
for this pass rather than chased
docs/ADMIN.md + docs/SPRINT-PLAN.md updated with the scope decision and
what was explicitly not done (Lighthouse, animations, SEO/sitemap,
storefront/editor a11y).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 00:14:46 +04:00
|
|
|
<select [attr.aria-label]="'adminUsers.role' | translate" [ngModel]="inviteRoleId()" (ngModelChange)="inviteRoleId.set($event)">
|
feat(admin): users and permissions
Sprint 25.
New features/admin/users/ module, net-new /:lang/backoffice/users route +
Dashboard Quick Action.
- users: name, Telegram username, scope (marketplace vs office admin),
role (inline change), status (active/invited/suspended), last login
- 4 built-in roles (owner/admin/editor/viewer) with flat permission lists
- invitations: email + role + scope form, pending list + revoke (no email
actually sends - local record only)
- passwordless login confirmed already real (AdminAuthService Telegram QR,
docs/BACKEND.md item 1) - linked, not reimplemented
- per-user mock session list (device/IP/last-active, revoke) - flagged as
mock since the real AdminAuthService only ever tracks the current
browser's session
- per-user audit log dialog (role/status changes), same pattern as
Sprint 24's per-transaction audit, intentionally separate from the
system-wide log planned for Sprint 26
docs/ADMIN.md + docs/BACKEND.md (new item 14) updated.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 11:05:21 +04:00
|
|
|
@for (role of facade.roles(); track role.id) {
|
|
|
|
|
<option [value]="role.id">{{ role.name }}</option>
|
|
|
|
|
}
|
|
|
|
|
</select>
|
refactor: marketplace release polish
Sprint 28, scoped to admin/* (user decision — full marketplace audit
declined in favor of a bounded pass over the 8 admin features from
Sprints 20-27).
- a11y: aria-label added to every bare <select> not already inside a
<label> across categories/products/orders/transactions/users/monitoring
- loading states: app-skeleton rows/cards added to list pages that
previously rendered blank during the initial fetch (categories, orders,
transactions, users, monitoring's event feed, analytics summary cards)
- admin-dashboard-card's custom shimmer CSS replaced with the shared
SkeletonComponent (same visual result, one less duplicated animation)
- bundle-size budget warning (~198kB over) confirmed pre-existing —
present at Sprint 20's first build before any admin/* code existed,
and new admin pages are all lazy-loaded — documented as out of scope
for this pass rather than chased
docs/ADMIN.md + docs/SPRINT-PLAN.md updated with the scope decision and
what was explicitly not done (Lighthouse, animations, SEO/sitemap,
storefront/editor a11y).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 00:14:46 +04:00
|
|
|
<select [attr.aria-label]="'adminUsers.scope' | translate" [ngModel]="inviteScope()" (ngModelChange)="inviteScope.set($event)">
|
feat(admin): users and permissions
Sprint 25.
New features/admin/users/ module, net-new /:lang/backoffice/users route +
Dashboard Quick Action.
- users: name, Telegram username, scope (marketplace vs office admin),
role (inline change), status (active/invited/suspended), last login
- 4 built-in roles (owner/admin/editor/viewer) with flat permission lists
- invitations: email + role + scope form, pending list + revoke (no email
actually sends - local record only)
- passwordless login confirmed already real (AdminAuthService Telegram QR,
docs/BACKEND.md item 1) - linked, not reimplemented
- per-user mock session list (device/IP/last-active, revoke) - flagged as
mock since the real AdminAuthService only ever tracks the current
browser's session
- per-user audit log dialog (role/status changes), same pattern as
Sprint 24's per-transaction audit, intentionally separate from the
system-wide log planned for Sprint 26
docs/ADMIN.md + docs/BACKEND.md (new item 14) updated.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 11:05:21 +04:00
|
|
|
<option value="marketplace">{{ 'adminUsers.scopeValue.marketplace' | translate }}</option>
|
|
|
|
|
<option value="office">{{ 'adminUsers.scopeValue.office' | translate }}</option>
|
|
|
|
|
</select>
|
|
|
|
|
<app-button variant="primary" (click)="sendInvite()">{{ 'adminUsers.sendInvite' | translate }}</app-button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
@if (facade.invitations().length > 0) {
|
|
|
|
|
<app-table>
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
2026-07-23 11:06:17 +04:00
|
|
|
<th scope="col">{{ 'adminUsers.email' | translate }}</th>
|
|
|
|
|
<th scope="col">{{ 'adminUsers.role' | translate }}</th>
|
|
|
|
|
<th scope="col">{{ 'backoffice.status' | translate }}</th>
|
|
|
|
|
<th scope="col">{{ 'adminProducts.actions' | translate }}</th>
|
feat(admin): users and permissions
Sprint 25.
New features/admin/users/ module, net-new /:lang/backoffice/users route +
Dashboard Quick Action.
- users: name, Telegram username, scope (marketplace vs office admin),
role (inline change), status (active/invited/suspended), last login
- 4 built-in roles (owner/admin/editor/viewer) with flat permission lists
- invitations: email + role + scope form, pending list + revoke (no email
actually sends - local record only)
- passwordless login confirmed already real (AdminAuthService Telegram QR,
docs/BACKEND.md item 1) - linked, not reimplemented
- per-user mock session list (device/IP/last-active, revoke) - flagged as
mock since the real AdminAuthService only ever tracks the current
browser's session
- per-user audit log dialog (role/status changes), same pattern as
Sprint 24's per-transaction audit, intentionally separate from the
system-wide log planned for Sprint 26
docs/ADMIN.md + docs/BACKEND.md (new item 14) updated.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 11:05:21 +04:00
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
@for (invite of facade.invitations(); track invite.id) {
|
|
|
|
|
<tr>
|
|
|
|
|
<td>{{ invite.email }}</td>
|
|
|
|
|
<td>{{ facade.roleName(invite.roleId) }}</td>
|
|
|
|
|
<td><app-badge variant="neutral">{{ ('adminUsers.invitationStatus.' + invite.status) | translate }}</app-badge></td>
|
|
|
|
|
<td>
|
|
|
|
|
@if (invite.status === 'pending') {
|
|
|
|
|
<app-button variant="danger" size="sm" (click)="facade.revokeInvitation(invite.id)">{{ 'adminUsers.revoke' | translate }}</app-button>
|
|
|
|
|
}
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
}
|
|
|
|
|
</tbody>
|
|
|
|
|
</app-table>
|
refactor: marketplace release polish
Sprint 28, scoped to admin/* (user decision — full marketplace audit
declined in favor of a bounded pass over the 8 admin features from
Sprints 20-27).
- a11y: aria-label added to every bare <select> not already inside a
<label> across categories/products/orders/transactions/users/monitoring
- loading states: app-skeleton rows/cards added to list pages that
previously rendered blank during the initial fetch (categories, orders,
transactions, users, monitoring's event feed, analytics summary cards)
- admin-dashboard-card's custom shimmer CSS replaced with the shared
SkeletonComponent (same visual result, one less duplicated animation)
- bundle-size budget warning (~198kB over) confirmed pre-existing —
present at Sprint 20's first build before any admin/* code existed,
and new admin pages are all lazy-loaded — documented as out of scope
for this pass rather than chased
docs/ADMIN.md + docs/SPRINT-PLAN.md updated with the scope decision and
what was explicitly not done (Lighthouse, animations, SEO/sitemap,
storefront/editor a11y).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 00:14:46 +04:00
|
|
|
} @else {
|
|
|
|
|
<app-empty-state [title]="'adminUsers.invitationsEmptyTitle' | translate" />
|
feat(admin): users and permissions
Sprint 25.
New features/admin/users/ module, net-new /:lang/backoffice/users route +
Dashboard Quick Action.
- users: name, Telegram username, scope (marketplace vs office admin),
role (inline change), status (active/invited/suspended), last login
- 4 built-in roles (owner/admin/editor/viewer) with flat permission lists
- invitations: email + role + scope form, pending list + revoke (no email
actually sends - local record only)
- passwordless login confirmed already real (AdminAuthService Telegram QR,
docs/BACKEND.md item 1) - linked, not reimplemented
- per-user mock session list (device/IP/last-active, revoke) - flagged as
mock since the real AdminAuthService only ever tracks the current
browser's session
- per-user audit log dialog (role/status changes), same pattern as
Sprint 24's per-transaction audit, intentionally separate from the
system-wide log planned for Sprint 26
docs/ADMIN.md + docs/BACKEND.md (new item 14) updated.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 11:05:21 +04:00
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
<h2>{{ 'adminUsers.roles' | translate }}</h2>
|
|
|
|
|
@for (role of facade.roles(); track role.id) {
|
|
|
|
|
<p><strong>{{ role.name }}</strong> — {{ role.permissions.join(', ') }}</p>
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<app-dialog [open]="!!facade.sessionsTarget()" [titleText]="'adminUsers.sessions' | translate" size="sm" (closed)="facade.closeSessions()">
|
|
|
|
|
@for (session of facade.sessions(); track session.id) {
|
|
|
|
|
<p>{{ session.device }} — {{ session.ip }} — {{ session.lastActiveAt | date:'short' }}
|
|
|
|
|
@if (session.current) { <app-badge variant="success">{{ 'adminUsers.currentSession' | translate }}</app-badge> }
|
|
|
|
|
@else { <app-button variant="danger" size="sm" (click)="facade.revokeSession(session.id)">{{ 'adminUsers.revoke' | translate }}</app-button> }
|
|
|
|
|
</p>
|
|
|
|
|
}
|
|
|
|
|
</app-dialog>
|
|
|
|
|
|
|
|
|
|
<app-dialog [open]="!!facade.auditTarget()" [titleText]="'adminTransactions.audit' | translate" size="sm" (closed)="facade.closeAudit()">
|
|
|
|
|
@for (entry of facade.audit(); track $index) {
|
|
|
|
|
<p>{{ entry.timestamp | date:'short' }} — {{ entry.actor }} — {{ entry.action }}</p>
|
|
|
|
|
}
|
|
|
|
|
</app-dialog>
|
|
|
|
|
</section>
|