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
|
|
|
import { Injectable, inject, signal } from '@angular/core';
|
|
|
|
|
import { take } from 'rxjs/operators';
|
|
|
|
|
import { AdminInvitation, AdminRole, AdminSession, AdminUser, AdminUserAuditEntry, AdminUserScope, AdminUserStatus } from '../models/admin-user.model';
|
|
|
|
|
import { AdminUsersLocalGateway } from '../services/admin-users-local.gateway';
|
|
|
|
|
|
|
|
|
|
@Injectable({ providedIn: 'root' })
|
|
|
|
|
export class AdminUsersFacade {
|
|
|
|
|
private readonly gateway = inject(AdminUsersLocalGateway);
|
|
|
|
|
|
|
|
|
|
readonly users = signal<AdminUser[]>([]);
|
|
|
|
|
readonly roles = signal<AdminRole[]>([]);
|
|
|
|
|
readonly invitations = signal<AdminInvitation[]>([]);
|
|
|
|
|
readonly loading = signal(false);
|
fix(backoffice): add error+retry states to Users, Monitoring, Analytics, Reports
Phase 8 (RC-01): these 4 list/dashboard pages had no error-state handling
on their primary data-load subscriptions — on a gateway error, `loading`
was either never reset (Users, Monitoring, Analytics: genuine infinite-
spinner risk, nested subscribe chain in Analytics never resolved on
failure) or there was no loading/empty/error handling at all (Reports
queue: raw table with zero skeleton or fallback).
- admin-users.facade.ts, admin-monitoring.facade.ts: add `error` signal,
error callback on the primary load subscribe so `loading` always
resolves.
- admin-analytics.facade.ts: add `error` signal; every level of the
4-deep nested gateway subscribe chain (orders -> products ->
categories -> reviews) now has an error handler that resolves loading
instead of leaving it stuck true.
- admin-moderation.facade.ts: add `reportsLoading`/`reportsError` signals
(reports list had none previously).
- Templates: reuse existing `app-skeleton`/`app-empty-state`/`app-button`
primitives for the new error branch, `common.retry` label, two new
generic `common.errorTitle`/`common.errorDescription` i18n keys added
to en/ru/hy (reused across all 4 fixes instead of one-off per-page
copy).
Verified: tsc --noEmit clean, `npm run build` green (pre-existing bundle-
budget warning only, unrelated). Live-checked Home (375px) and Backoffice
Products (1024px) — no console errors, tables/cards render without
overflow.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 21:31:30 +04:00
|
|
|
readonly error = signal(false);
|
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
|
|
|
readonly sessionsTarget = signal<AdminUser | null>(null);
|
|
|
|
|
readonly sessions = signal<AdminSession[]>([]);
|
|
|
|
|
readonly auditTarget = signal<AdminUser | null>(null);
|
|
|
|
|
readonly audit = signal<AdminUserAuditEntry[]>([]);
|
|
|
|
|
|
|
|
|
|
loadAll(): void {
|
|
|
|
|
this.loading.set(true);
|
fix(backoffice): add error+retry states to Users, Monitoring, Analytics, Reports
Phase 8 (RC-01): these 4 list/dashboard pages had no error-state handling
on their primary data-load subscriptions — on a gateway error, `loading`
was either never reset (Users, Monitoring, Analytics: genuine infinite-
spinner risk, nested subscribe chain in Analytics never resolved on
failure) or there was no loading/empty/error handling at all (Reports
queue: raw table with zero skeleton or fallback).
- admin-users.facade.ts, admin-monitoring.facade.ts: add `error` signal,
error callback on the primary load subscribe so `loading` always
resolves.
- admin-analytics.facade.ts: add `error` signal; every level of the
4-deep nested gateway subscribe chain (orders -> products ->
categories -> reviews) now has an error handler that resolves loading
instead of leaving it stuck true.
- admin-moderation.facade.ts: add `reportsLoading`/`reportsError` signals
(reports list had none previously).
- Templates: reuse existing `app-skeleton`/`app-empty-state`/`app-button`
primitives for the new error branch, `common.retry` label, two new
generic `common.errorTitle`/`common.errorDescription` i18n keys added
to en/ru/hy (reused across all 4 fixes instead of one-off per-page
copy).
Verified: tsc --noEmit clean, `npm run build` green (pre-existing bundle-
budget warning only, unrelated). Live-checked Home (375px) and Backoffice
Products (1024px) — no console errors, tables/cards render without
overflow.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 21:31:30 +04:00
|
|
|
this.error.set(false);
|
|
|
|
|
this.gateway.loadUsers().pipe(take(1)).subscribe({
|
|
|
|
|
next: users => { this.users.set(users); this.loading.set(false); },
|
|
|
|
|
error: () => { this.users.set([]); this.loading.set(false); this.error.set(true); }
|
|
|
|
|
});
|
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
|
|
|
this.gateway.loadRoles().pipe(take(1)).subscribe(roles => this.roles.set(roles));
|
|
|
|
|
this.gateway.loadInvitations().pipe(take(1)).subscribe(invitations => this.invitations.set(invitations));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
roleName(roleId: string): string {
|
|
|
|
|
return this.roles().find(role => role.id === roleId)?.name ?? roleId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setRole(userId: string, roleId: string): void {
|
|
|
|
|
this.gateway.setUserRole(userId, roleId).pipe(take(1)).subscribe({ next: () => this.loadAll() });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setStatus(userId: string, status: AdminUserStatus): void {
|
|
|
|
|
this.gateway.setUserStatus(userId, status).pipe(take(1)).subscribe({ next: () => this.loadAll() });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
invite(email: string, roleId: string, scope: AdminUserScope): void {
|
|
|
|
|
if (!email.trim()) return;
|
|
|
|
|
this.gateway.inviteUser(email.trim(), roleId, scope).pipe(take(1)).subscribe({ next: () => this.loadAll() });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
revokeInvitation(id: string): void {
|
|
|
|
|
this.gateway.revokeInvitation(id).pipe(take(1)).subscribe({ next: () => this.loadAll() });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
openSessions(user: AdminUser): void {
|
|
|
|
|
this.sessionsTarget.set(user);
|
|
|
|
|
this.gateway.loadSessions(user.id).pipe(take(1)).subscribe(sessions => this.sessions.set(sessions));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
closeSessions(): void {
|
|
|
|
|
this.sessionsTarget.set(null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
revokeSession(sessionId: string): void {
|
|
|
|
|
this.gateway.revokeSession(sessionId).pipe(take(1)).subscribe({
|
|
|
|
|
next: () => {
|
|
|
|
|
const user = this.sessionsTarget();
|
|
|
|
|
if (user) this.openSessions(user);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
openAudit(user: AdminUser): void {
|
|
|
|
|
this.auditTarget.set(user);
|
|
|
|
|
this.gateway.loadAudit(user.id).pipe(take(1)).subscribe(entries => this.audit.set(entries));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
closeAudit(): void {
|
|
|
|
|
this.auditTarget.set(null);
|
|
|
|
|
}
|
|
|
|
|
}
|