fix(backoffice): wording quality pass across orders, moderation, transactions, users, customers

- Replaced hardcoded English audit/timeline text (order status changes,
  review moderation events, user role/status changes) with proper
  adminXxx.timelineEvent.*/adminUsers.audit.* i18n keys, so Recent
  Activity/Timeline/Audit panels no longer mix English into ru/hy UI.
- Translated raw internal codes rendered directly to users: transaction
  payment method ('card'/'qr'/'cash_on_delivery' -> adminTransactions.methodValue.*)
  and user roles/permissions ('products.manage' etc -> adminUsers.roleValue.*/
  adminUsers.permission.*), replacing developer-facing enum leakage with
  real copy.
- Fixed wrong-noun list-footer counts: Orders/Transactions/Moderation
  list pages all reused adminProducts.items ("N товаров"/"N products")
  regardless of what was actually listed; each now has its own itemsCount
  key ("N заказов", "N транзакций", "N отзывов").
- Fixed customer detail page's "Back" button reusing adminOrders.back
  ("Back to orders") instead of a customers-specific label.
- Added translation keys to en/ru/hy + translations.ts interface for all
  of the above.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
sdarbinyan
2026-07-25 21:20:37 +04:00
parent 72846b44b1
commit b909a195f7
28 changed files with 311 additions and 42 deletions

View File

@@ -16,7 +16,7 @@
} @else {
<ul>
@for (entry of stats().recentActivity; track entry.timestamp + entry.reviewId) {
<li>{{ entry.timestamp | date:'short' }} — {{ entry.productName }} — {{ entry.action }}</li>
<li>{{ entry.timestamp | date:'short' }} — {{ entry.productName }} — {{ activityText(entry) }}</li>
}
</ul>
}

View File

@@ -1,6 +1,7 @@
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
import { ChangeDetectionStrategy, Component, inject, input } from '@angular/core';
import { DatePipe } from '@angular/common';
import { TranslatePipe } from '../../../../../i18n/translate.pipe';
import { TranslateService } from '../../../../../i18n/translate.service';
import { CardComponent } from '../../../../../shared/ui/card/card.component';
import { DashboardMetricComponent } from '../../../dashboard/components/dashboard-metric.component';
import { AdminModerationDashboardStats } from '../../facade/admin-moderation.facade';
@@ -14,5 +15,12 @@ import { AdminModerationDashboardStats } from '../../facade/admin-moderation.fac
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ModerationDashboardComponent {
private readonly translate = inject(TranslateService);
readonly stats = input.required<AdminModerationDashboardStats>();
activityText(entry: AdminModerationDashboardStats['recentActivity'][number]): string {
const status = entry.status ? this.translate.t('adminModeration.status.' + entry.status) : '';
return this.translate.t('adminModeration.timelineEvent.' + entry.eventKey, { status });
}
}

View File

@@ -28,7 +28,7 @@ export interface AdminModerationDashboardStats {
reported: number;
spam: number;
averageRating: number | null;
recentActivity: { reviewId: string; productName: string; action: string; timestamp: string }[];
recentActivity: { reviewId: string; productName: string; eventKey: string; status?: AdminReviewStatus; timestamp: string }[];
moderationHealthPercent: number;
}
@@ -197,7 +197,7 @@ export class AdminModerationFacade {
const averageRating = rated.length > 0 ? Math.round((rated.reduce((sum, review) => sum + review.rating, 0) / rated.length) * 10) / 10 : null;
const recentActivity = reviews
.flatMap(review => review.timeline.map(entry => ({ reviewId: review.id, productName: review.productName, action: entry.action, timestamp: entry.timestamp })))
.flatMap(review => review.timeline.map(entry => ({ reviewId: review.id, productName: review.productName, eventKey: entry.eventKey, status: entry.status, timestamp: entry.timestamp })))
.sort((a, b) => b.timestamp.localeCompare(a.timestamp))
.slice(0, 8);

View File

@@ -1,8 +1,11 @@
export type AdminReviewStatus = 'pending' | 'approved' | 'rejected' | 'spam';
export type AdminReviewTimelineEventKey = 'submitted' | 'statusChanged' | 'restored' | 'hidden';
export type AdminReviewTimelineActor = 'admin' | 'customer';
export interface AdminReviewTimelineEntry {
action: string;
actor: string;
eventKey: AdminReviewTimelineEventKey;
actor: AdminReviewTimelineActor;
status?: AdminReviewStatus;
note: string;
timestamp: string;
}

View File

@@ -4,6 +4,7 @@ import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { AdminModerationFacade } from '../facade/admin-moderation.facade';
import { TranslatePipe } from '../../../../i18n/translate.pipe';
import { TranslateService } from '../../../../i18n/translate.service';
import { LanguageService } from '../../../../services/language.service';
import { ButtonComponent } from '../../../../shared/ui/button/button.component';
import { BadgeComponent } from '../../../../shared/ui/badge/badge.component';
@@ -22,6 +23,7 @@ export class AdminReviewDetailPageComponent {
private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router);
private readonly languageService = inject(LanguageService);
private readonly translate = inject(TranslateService);
readonly noteDraft = signal('');
@@ -34,7 +36,14 @@ export class AdminReviewDetailPageComponent {
timelineEntries(): OrderTimelineEntry[] {
const review = this.facade.selected();
return review ? review.timeline.map(entry => ({ timestamp: entry.timestamp, note: `${entry.action}${entry.note ? ' — ' + entry.note : ''} (${entry.actor})` })) : [];
if (!review) return [];
return review.timeline.map(entry => {
const status = entry.status ? this.translate.t('adminModeration.status.' + entry.status) : '';
const eventText = this.translate.t('adminModeration.timelineEvent.' + entry.eventKey, { status });
const actor = this.translate.t('adminModeration.actor.' + entry.actor);
const note = entry.note ? `${eventText}${entry.note}` : eventText;
return { timestamp: entry.timestamp, note: `${note} (${actor})` };
});
}
back(): void {

View File

@@ -127,7 +127,7 @@
@if (facade.reviews().length > 0) {
<div class="pager">
<span>{{ facade.total() }} {{ 'adminProducts.items' | translate }}</span>
<span>{{ facade.total() }} {{ 'adminModeration.itemsCount' | translate }}</span>
<app-pagination [currentPage]="facade.filters().page" [totalPages]="totalPages()" (pageChange)="facade.updateFilters({ page: $event })" />
</div>
}

View File

@@ -3,7 +3,7 @@ import { Observable, of } from 'rxjs';
import { delay } from 'rxjs/operators';
import { BackofficeDataService } from '../../../../core/backoffice/backoffice-data.service';
import { ProductCardConfig } from '../../../../shared/models/ui';
import { AdminReview, AdminReviewListFilters, AdminReviewsListResult, AdminReviewStatus } from '../models/admin-review.model';
import { AdminReview, AdminReviewListFilters, AdminReviewsListResult, AdminReviewStatus, AdminReviewTimelineEventKey } from '../models/admin-review.model';
import { AdminReport, AdminReportStatus } from '../models/admin-report.model';
import { AdminModerationGateway } from './admin-moderation-gateway.interface';
@@ -61,16 +61,17 @@ export class AdminModerationLocalGateway implements AdminModerationGateway {
...review,
status,
updatedAt: new Date().toISOString(),
timeline: [...review.timeline, { action: `Status set to ${status}`, actor: 'Admin', note, timestamp: new Date().toISOString() }],
timeline: [...review.timeline, { eventKey: 'statusChanged' as const, status, actor: 'admin' as const, note, timestamp: new Date().toISOString() }],
}));
}
setReviewVisible(id: string, visible: boolean): Observable<AdminReview | null> {
const eventKey: AdminReviewTimelineEventKey = visible ? 'restored' : 'hidden';
return this.mutate(id, review => ({
...review,
visible,
updatedAt: new Date().toISOString(),
timeline: [...review.timeline, { action: visible ? 'Restored' : 'Hidden', actor: 'Admin', note: '', timestamp: new Date().toISOString() }],
timeline: [...review.timeline, { eventKey, actor: 'admin' as const, note: '', timestamp: new Date().toISOString() }],
}));
}
@@ -158,7 +159,7 @@ export class AdminModerationLocalGateway implements AdminModerationGateway {
featured: false,
reportCount,
moderatorNotes: '',
timeline: [{ action: 'Submitted', actor: 'Customer', note: '', timestamp: createdAt }],
timeline: [{ eventKey: 'submitted' as const, actor: 'customer' as const, note: '', timestamp: createdAt }],
createdAt,
updatedAt: createdAt,
};