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:
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user