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>
This commit is contained in:
sdarbinyan
2026-07-25 21:31:30 +04:00
parent b909a195f7
commit e153a67ec0
14 changed files with 113 additions and 38 deletions

View File

@@ -47,6 +47,8 @@ export class AdminModerationFacade {
readonly loading = signal(false);
readonly selected = signal<AdminReview | null>(null);
readonly reports = signal<AdminReport[]>([]);
readonly reportsLoading = signal(false);
readonly reportsError = signal(false);
readonly viewMode = signal<AdminModerationViewMode>((this.localStorage.getItem(VIEW_MODE_KEY) as AdminModerationViewMode) || 'table');
readonly density = signal<AdminModerationDensity>((this.localStorage.getItem(DENSITY_KEY) as AdminModerationDensity) || 'comfortable');
@@ -167,7 +169,12 @@ export class AdminModerationFacade {
}
loadReports(): void {
this.gateway.loadReports().pipe(take(1)).subscribe({ next: reports => this.reports.set(reports) });
this.reportsLoading.set(true);
this.reportsError.set(false);
this.gateway.loadReports().pipe(take(1)).subscribe({
next: reports => { this.reports.set(reports); this.reportsLoading.set(false); },
error: () => { this.reports.set([]); this.reportsLoading.set(false); this.reportsError.set(true); }
});
}
setReportStatus(id: string, status: AdminReportStatus): void {

View File

@@ -4,7 +4,18 @@
<h1>{{ 'adminModeration.reportsQueue' | translate }}</h1>
</header>
@if (facade.reports().length === 0) {
@if (facade.reportsLoading()) {
<div class="skeleton-rows" role="status" aria-live="polite" aria-busy="true">
@for (i of [1,2,3,4]; track i) { <app-skeleton shape="rect" height="40px" /> }
<span class="sr-only">{{ 'common.loading' | translate }}</span>
</div>
} @else if (facade.reportsError()) {
<app-empty-state [title]="'common.errorTitle' | translate" [description]="'common.errorDescription' | translate">
<span slot="actions">
<app-button variant="primary" (click)="facade.loadReports()">{{ 'common.retry' | translate }}</app-button>
</span>
</app-empty-state>
} @else if (facade.reports().length === 0) {
<app-empty-state [title]="'adminModeration.reportsEmptyTitle' | translate" [description]="'adminModeration.reportsEmptyDescription' | translate" />
} @else {
<app-table>

View File

@@ -9,11 +9,12 @@ import { ButtonComponent } from '../../../../shared/ui/button/button.component';
import { BadgeComponent } from '../../../../shared/ui/badge/badge.component';
import { TableComponent } from '../../../../shared/ui/table/table.component';
import { EmptyStateComponent } from '../../../../shared/ui/empty-state/empty-state.component';
import { SkeletonComponent } from '../../../../shared/ui/skeleton/skeleton.component';
@Component({
selector: 'app-admin-reports-list-page',
standalone: true,
imports: [CommonModule, TranslatePipe, ButtonComponent, BadgeComponent, TableComponent, EmptyStateComponent],
imports: [CommonModule, TranslatePipe, ButtonComponent, BadgeComponent, TableComponent, EmptyStateComponent, SkeletonComponent],
templateUrl: './admin-reports-list-page.component.html',
styleUrls: ['./admin-reports-list-page.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush