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

@@ -1,7 +1,7 @@
@if (facade.selected(); as customer) { @if (facade.selected(); as customer) {
<main class="customer-detail"> <main class="customer-detail">
<header class="toolbar"> <header class="toolbar">
<app-button variant="secondary" size="sm" (click)="back()">{{ 'adminOrders.back' | translate }}</app-button> <app-button variant="secondary" size="sm" (click)="back()">{{ 'adminCustomers.back' | translate }}</app-button>
<h1>{{ customer.name }}</h1> <h1>{{ customer.name }}</h1>
@if (customer.orderCount > 1) { @if (customer.orderCount > 1) {
<app-badge variant="success">{{ 'adminCustomers.returning' | translate }}</app-badge> <app-badge variant="success">{{ 'adminCustomers.returning' | translate }}</app-badge>

View File

@@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { AdminCustomersFacade } from '../facade/admin-customers.facade'; import { AdminCustomersFacade } from '../facade/admin-customers.facade';
import { TranslatePipe } from '../../../../i18n/translate.pipe'; import { TranslatePipe } from '../../../../i18n/translate.pipe';
import { TranslateService } from '../../../../i18n/translate.service';
import { LanguageService } from '../../../../services/language.service'; import { LanguageService } from '../../../../services/language.service';
import { ButtonComponent } from '../../../../shared/ui/button/button.component'; import { ButtonComponent } from '../../../../shared/ui/button/button.component';
import { BadgeComponent } from '../../../../shared/ui/badge/badge.component'; import { BadgeComponent } from '../../../../shared/ui/badge/badge.component';
@@ -21,12 +22,20 @@ export class AdminCustomerDetailPageComponent {
private readonly route = inject(ActivatedRoute); private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router); private readonly router = inject(Router);
private readonly languageService = inject(LanguageService); private readonly languageService = inject(LanguageService);
private readonly translate = inject(TranslateService);
readonly activity = computed<OrderTimelineEntry[]>(() => { readonly activity = computed<OrderTimelineEntry[]>(() => {
const customer = this.facade.selected(); const customer = this.facade.selected();
if (!customer) return []; if (!customer) return [];
return customer.orders return customer.orders
.flatMap(order => order.timeline.map(entry => ({ status: entry.status, timestamp: entry.timestamp, note: entry.note, orderNumber: order.orderNumber }))) .flatMap(order => order.timeline.map(entry => ({
status: entry.status,
timestamp: entry.timestamp,
note: this.translate.t('adminOrders.timelineEvent.' + entry.eventKey, {
status: this.translate.t('adminOrders.status.' + entry.status),
}),
orderNumber: order.orderNumber,
})))
.sort((a, b) => b.timestamp.localeCompare(a.timestamp)); .sort((a, b) => b.timestamp.localeCompare(a.timestamp));
}); });

View File

@@ -16,7 +16,7 @@
} @else { } @else {
<ul> <ul>
@for (entry of stats().recentActivity; track entry.timestamp + entry.reviewId) { @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> </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 { DatePipe } from '@angular/common';
import { TranslatePipe } from '../../../../../i18n/translate.pipe'; import { TranslatePipe } from '../../../../../i18n/translate.pipe';
import { TranslateService } from '../../../../../i18n/translate.service';
import { CardComponent } from '../../../../../shared/ui/card/card.component'; import { CardComponent } from '../../../../../shared/ui/card/card.component';
import { DashboardMetricComponent } from '../../../dashboard/components/dashboard-metric.component'; import { DashboardMetricComponent } from '../../../dashboard/components/dashboard-metric.component';
import { AdminModerationDashboardStats } from '../../facade/admin-moderation.facade'; import { AdminModerationDashboardStats } from '../../facade/admin-moderation.facade';
@@ -14,5 +15,12 @@ import { AdminModerationDashboardStats } from '../../facade/admin-moderation.fac
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class ModerationDashboardComponent { export class ModerationDashboardComponent {
private readonly translate = inject(TranslateService);
readonly stats = input.required<AdminModerationDashboardStats>(); 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; reported: number;
spam: number; spam: number;
averageRating: number | null; 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; 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 averageRating = rated.length > 0 ? Math.round((rated.reduce((sum, review) => sum + review.rating, 0) / rated.length) * 10) / 10 : null;
const recentActivity = reviews 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)) .sort((a, b) => b.timestamp.localeCompare(a.timestamp))
.slice(0, 8); .slice(0, 8);

View File

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

View File

@@ -4,6 +4,7 @@ import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { AdminModerationFacade } from '../facade/admin-moderation.facade'; import { AdminModerationFacade } from '../facade/admin-moderation.facade';
import { TranslatePipe } from '../../../../i18n/translate.pipe'; import { TranslatePipe } from '../../../../i18n/translate.pipe';
import { TranslateService } from '../../../../i18n/translate.service';
import { LanguageService } from '../../../../services/language.service'; import { LanguageService } from '../../../../services/language.service';
import { ButtonComponent } from '../../../../shared/ui/button/button.component'; import { ButtonComponent } from '../../../../shared/ui/button/button.component';
import { BadgeComponent } from '../../../../shared/ui/badge/badge.component'; import { BadgeComponent } from '../../../../shared/ui/badge/badge.component';
@@ -22,6 +23,7 @@ export class AdminReviewDetailPageComponent {
private readonly route = inject(ActivatedRoute); private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router); private readonly router = inject(Router);
private readonly languageService = inject(LanguageService); private readonly languageService = inject(LanguageService);
private readonly translate = inject(TranslateService);
readonly noteDraft = signal(''); readonly noteDraft = signal('');
@@ -34,7 +36,14 @@ export class AdminReviewDetailPageComponent {
timelineEntries(): OrderTimelineEntry[] { timelineEntries(): OrderTimelineEntry[] {
const review = this.facade.selected(); 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 { back(): void {

View File

@@ -127,7 +127,7 @@
@if (facade.reviews().length > 0) { @if (facade.reviews().length > 0) {
<div class="pager"> <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 })" /> <app-pagination [currentPage]="facade.filters().page" [totalPages]="totalPages()" (pageChange)="facade.updateFilters({ page: $event })" />
</div> </div>
} }

View File

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

View File

@@ -5,6 +5,7 @@ import { TranslatePipe } from '../../../../../i18n/translate.pipe';
export interface OrderTimelineEntry { export interface OrderTimelineEntry {
status?: string; status?: string;
timestamp: string; timestamp: string;
/** Pre-resolved, already-translated display text for this event. */
note: string; note: string;
orderNumber?: string; orderNumber?: string;
} }

View File

@@ -25,7 +25,7 @@
} @else { } @else {
<ul> <ul>
@for (entry of stats().recentActivity; track entry.timestamp + entry.orderId) { @for (entry of stats().recentActivity; track entry.timestamp + entry.orderId) {
<li>{{ entry.timestamp | date:'short' }} — {{ entry.orderNumber }} — {{ entry.note }}</li> <li>{{ entry.timestamp | date:'short' }} — {{ entry.orderNumber }} — {{ activityText(entry) }}</li>
} }
</ul> </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 { DatePipe } from '@angular/common';
import { TranslatePipe } from '../../../../../i18n/translate.pipe'; import { TranslatePipe } from '../../../../../i18n/translate.pipe';
import { TranslateService } from '../../../../../i18n/translate.service';
import { CardComponent } from '../../../../../shared/ui/card/card.component'; import { CardComponent } from '../../../../../shared/ui/card/card.component';
import { BadgeComponent } from '../../../../../shared/ui/badge/badge.component'; import { BadgeComponent } from '../../../../../shared/ui/badge/badge.component';
import { DashboardMetricComponent } from '../../../dashboard/components/dashboard-metric.component'; import { DashboardMetricComponent } from '../../../dashboard/components/dashboard-metric.component';
@@ -15,5 +16,13 @@ import { AdminOrdersDashboardStats } from '../../facade/admin-orders.facade';
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class OrdersDashboardComponent { export class OrdersDashboardComponent {
private readonly translate = inject(TranslateService);
readonly stats = input.required<AdminOrdersDashboardStats>(); readonly stats = input.required<AdminOrdersDashboardStats>();
activityText(entry: AdminOrdersDashboardStats['recentActivity'][number]): string {
return this.translate.t('adminOrders.timelineEvent.' + entry.eventKey, {
status: this.translate.t('adminOrders.status.' + entry.status),
});
}
} }

View File

@@ -20,7 +20,7 @@ export interface AdminOrdersDashboardStats {
returningCustomers: number; returningCustomers: number;
averageOrder: number; averageOrder: number;
currency: string; currency: string;
recentActivity: { orderNumber: string; orderId: string; note: string; status: AdminOrderStatus; timestamp: string }[]; recentActivity: { orderNumber: string; orderId: string; eventKey: string; status: AdminOrderStatus; timestamp: string }[];
alerts: { labelKey: string; count: number }[]; alerts: { labelKey: string; count: number }[];
} }
@@ -99,12 +99,12 @@ export class AdminOrdersFacade {
this.gateway.loadOrder(id).pipe(take(1)).subscribe({ next: order => this.selected.set(order) }); this.gateway.loadOrder(id).pipe(take(1)).subscribe({ next: order => this.selected.set(order) });
} }
setStatus(id: string, status: AdminOrderStatus, note: string): void { setStatus(id: string, status: AdminOrderStatus): void {
this.gateway.updateStatus(id, status, note).pipe(take(1)).subscribe({ next: order => this.selected.set(order) }); this.gateway.updateStatus(id, status).pipe(take(1)).subscribe({ next: order => this.selected.set(order) });
} }
cancelOrder(id: string): void { cancelOrder(id: string): void {
this.setStatus(id, 'cancelled', 'Cancelled by admin'); this.setStatus(id, 'cancelled');
} }
requestRefund(id: string): void { requestRefund(id: string): void {
@@ -118,7 +118,7 @@ export class AdminOrdersFacade {
applyBulkStatus(status: AdminOrderStatus): void { applyBulkStatus(status: AdminOrderStatus): void {
const ids = [...this.selectedIds()]; const ids = [...this.selectedIds()];
ids.forEach(id => this.gateway.updateStatus(id, status, `Bulk status change to ${status}`).pipe(take(1)).subscribe()); ids.forEach(id => this.gateway.updateStatus(id, status).pipe(take(1)).subscribe());
this.clearSelection(); this.clearSelection();
this.loadList(); this.loadList();
this.loadDashboardStats(); this.loadDashboardStats();
@@ -185,7 +185,7 @@ export class AdminOrdersFacade {
const averageOrder = orders.length > 0 ? Math.round(total / orders.length) : 0; const averageOrder = orders.length > 0 ? Math.round(total / orders.length) : 0;
const recentActivity = orders const recentActivity = orders
.flatMap(order => order.timeline.map(entry => ({ orderNumber: order.orderNumber, orderId: order.id, note: entry.note, status: entry.status, timestamp: entry.timestamp }))) .flatMap(order => order.timeline.map(entry => ({ orderNumber: order.orderNumber, orderId: order.id, eventKey: entry.eventKey, status: entry.status, timestamp: entry.timestamp })))
.sort((a, b) => b.timestamp.localeCompare(a.timestamp)) .sort((a, b) => b.timestamp.localeCompare(a.timestamp))
.slice(0, 8); .slice(0, 8);

View File

@@ -27,10 +27,12 @@ export interface AdminOrderItem {
price: number; price: number;
} }
export type AdminOrderTimelineEventKey = 'created' | 'statusChanged' | 'refundRequested';
export interface AdminOrderTimelineEntry { export interface AdminOrderTimelineEntry {
status: AdminOrderStatus; status: AdminOrderStatus;
timestamp: string; timestamp: string;
note: string; eventKey: AdminOrderTimelineEventKey;
} }
export interface AdminOrder { export interface AdminOrder {

View File

@@ -68,7 +68,7 @@
<section class="card"> <section class="card">
<h3>{{ 'adminOrders.timeline' | translate }}</h3> <h3>{{ 'adminOrders.timeline' | translate }}</h3>
<app-order-timeline [entries]="order.timeline" /> <app-order-timeline [entries]="timelineEntries()" />
</section> </section>
<section class="grid two no-print"> <section class="grid two no-print">

View File

@@ -9,7 +9,7 @@ import { TranslateService } from '../../../../i18n/translate.service';
import { LanguageService } from '../../../../services/language.service'; import { LanguageService } from '../../../../services/language.service';
import { ButtonComponent } from '../../../../shared/ui/button/button.component'; import { ButtonComponent } from '../../../../shared/ui/button/button.component';
import { BadgeComponent } from '../../../../shared/ui/badge/badge.component'; import { BadgeComponent } from '../../../../shared/ui/badge/badge.component';
import { OrderTimelineComponent } from '../components/order-timeline/order-timeline.component'; import { OrderTimelineComponent, OrderTimelineEntry } from '../components/order-timeline/order-timeline.component';
const WORKFLOW_STEPS: AdminOrderStatus[] = ['pending', 'processing', 'shipped', 'delivered']; const WORKFLOW_STEPS: AdminOrderStatus[] = ['pending', 'processing', 'shipped', 'delivered'];
const TERMINAL_STATUSES: AdminOrderStatus[] = ['cancelled', 'refunded']; const TERMINAL_STATUSES: AdminOrderStatus[] = ['cancelled', 'refunded'];
@@ -45,6 +45,18 @@ export class AdminOrderDetailPageComponent {
return WORKFLOW_STEPS.indexOf(order.status); return WORKFLOW_STEPS.indexOf(order.status);
}); });
readonly timelineEntries = computed<OrderTimelineEntry[]>(() => {
const order = this.facade.selected();
if (!order) return [];
return order.timeline.map(entry => ({
status: entry.status,
timestamp: entry.timestamp,
note: this.translate.t('adminOrders.timelineEvent.' + entry.eventKey, {
status: this.translate.t('adminOrders.status.' + entry.status),
}),
}));
});
constructor() { constructor() {
const id = this.route.snapshot.paramMap.get('id'); const id = this.route.snapshot.paramMap.get('id');
if (id) { if (id) {
@@ -61,7 +73,7 @@ export class AdminOrderDetailPageComponent {
} }
setStatus(id: string, status: AdminOrderStatus): void { setStatus(id: string, status: AdminOrderStatus): void {
this.facade.setStatus(id, status, `Status changed to ${status}`); this.facade.setStatus(id, status);
} }
cancel(id: string): void { cancel(id: string): void {

View File

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

View File

@@ -4,7 +4,7 @@ import { AdminOrder, AdminOrderListFilters, AdminOrdersListResult, AdminOrderSta
export interface AdminOrdersGateway { export interface AdminOrdersGateway {
loadOrders(filters: AdminOrderListFilters): Observable<AdminOrdersListResult>; loadOrders(filters: AdminOrderListFilters): Observable<AdminOrdersListResult>;
loadOrder(id: string): Observable<AdminOrder | null>; loadOrder(id: string): Observable<AdminOrder | null>;
updateStatus(id: string, status: AdminOrderStatus, note: string): Observable<AdminOrder | null>; updateStatus(id: string, status: AdminOrderStatus): Observable<AdminOrder | null>;
requestRefund(id: string): Observable<AdminOrder | null>; requestRefund(id: string): Observable<AdminOrder | null>;
addNote(id: string, note: string, internal: boolean): Observable<AdminOrder | null>; addNote(id: string, note: string, internal: boolean): Observable<AdminOrder | null>;
archiveOrder(id: string): Observable<AdminOrder | null>; archiveOrder(id: string): Observable<AdminOrder | null>;

View File

@@ -31,12 +31,12 @@ export class AdminOrdersLocalGateway implements AdminOrdersGateway {
return of(this.ensureData().find(order => order.id === id) ?? null).pipe(delay(50)); return of(this.ensureData().find(order => order.id === id) ?? null).pipe(delay(50));
} }
updateStatus(id: string, status: AdminOrderStatus, note: string): Observable<AdminOrder | null> { updateStatus(id: string, status: AdminOrderStatus): Observable<AdminOrder | null> {
return this.mutate(id, order => ({ return this.mutate(id, order => ({
...order, ...order,
status, status,
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
timeline: [...order.timeline, { status, timestamp: new Date().toISOString(), note }], timeline: [...order.timeline, { status, timestamp: new Date().toISOString(), eventKey: 'statusChanged' as const }],
})); }));
} }
@@ -45,7 +45,7 @@ export class AdminOrdersLocalGateway implements AdminOrdersGateway {
...order, ...order,
payment: { ...order.payment, status: 'refund_requested' }, payment: { ...order.payment, status: 'refund_requested' },
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
timeline: [...order.timeline, { status: order.status, timestamp: new Date().toISOString(), note: 'Refund requested' }], timeline: [...order.timeline, { status: order.status, timestamp: new Date().toISOString(), eventKey: 'refundRequested' as const }],
})); }));
} }
@@ -122,8 +122,8 @@ export class AdminOrdersLocalGateway implements AdminOrdersGateway {
notes: '', notes: '',
internalNotes: '', internalNotes: '',
timeline: [ timeline: [
{ status: 'pending', timestamp: createdAt, note: 'Order created' }, { status: 'pending', timestamp: createdAt, eventKey: 'created' },
...(status !== 'pending' ? [{ status, timestamp: createdAt, note: `Status set to ${status}` }] : []), ...(status !== 'pending' ? [{ status, timestamp: createdAt, eventKey: 'statusChanged' as const }] : []),
], ],
archived: false, archived: false,
createdAt, createdAt,

View File

@@ -42,7 +42,7 @@
<tr> <tr>
<th scope="row">{{ tx.orderNumber }}</th> <th scope="row">{{ tx.orderNumber }}</th>
<td>{{ ('adminTransactions.typeValue.' + tx.type) | translate }}</td> <td>{{ ('adminTransactions.typeValue.' + tx.type) | translate }}</td>
<td>{{ tx.method }}</td> <td>{{ ('adminTransactions.methodValue.' + tx.method) | translate }}</td>
<td><app-badge [variant]="tx.status === 'success' ? 'success' : tx.status === 'failed' ? 'danger' : 'neutral'">{{ ('adminTransactions.status.' + tx.status) | translate }}</app-badge></td> <td><app-badge [variant]="tx.status === 'success' ? 'success' : tx.status === 'failed' ? 'danger' : 'neutral'">{{ ('adminTransactions.status.' + tx.status) | translate }}</app-badge></td>
<td>{{ tx.amount }} {{ tx.currency }}</td> <td>{{ tx.amount }} {{ tx.currency }}</td>
<td> <td>
@@ -63,7 +63,7 @@
</app-table> </app-table>
<div class="pager"> <div class="pager">
<span>{{ facade.total() }} {{ 'adminProducts.items' | translate }}</span> <span>{{ facade.total() }} {{ 'adminTransactions.itemsCount' | translate }}</span>
<app-pagination [currentPage]="facade.filters().page" [totalPages]="totalPages()" (pageChange)="facade.updateFilters({ page: $event })" /> <app-pagination [currentPage]="facade.filters().page" [totalPages]="totalPages()" (pageChange)="facade.updateFilters({ page: $event })" />
</div> </div>
} }

View File

@@ -41,7 +41,9 @@ export interface AdminSession {
} }
export interface AdminUserAuditEntry { export interface AdminUserAuditEntry {
action: string; eventKey: 'roleChanged' | 'statusChanged';
roleId?: string;
status?: AdminUserStatus;
actor: string; actor: string;
timestamp: string; timestamp: string;
} }

View File

@@ -28,7 +28,7 @@
<td> <td>
<select [attr.aria-label]="'adminUsers.role' | translate" [ngModel]="user.roleId" (ngModelChange)="facade.setRole(user.id, $event)"> <select [attr.aria-label]="'adminUsers.role' | translate" [ngModel]="user.roleId" (ngModelChange)="facade.setRole(user.id, $event)">
@for (role of facade.roles(); track role.id) { @for (role of facade.roles(); track role.id) {
<option [value]="role.id">{{ role.name }}</option> <option [value]="role.id">{{ roleLabel(role.id) }}</option>
} }
</select> </select>
</td> </td>
@@ -53,7 +53,7 @@
<app-input type="email" [ngModel]="inviteEmail()" (ngModelChange)="inviteEmail.set($event)" [placeholder]="'adminUsers.email' | translate" /> <app-input type="email" [ngModel]="inviteEmail()" (ngModelChange)="inviteEmail.set($event)" [placeholder]="'adminUsers.email' | translate" />
<select [attr.aria-label]="'adminUsers.role' | translate" [ngModel]="inviteRoleId()" (ngModelChange)="inviteRoleId.set($event)"> <select [attr.aria-label]="'adminUsers.role' | translate" [ngModel]="inviteRoleId()" (ngModelChange)="inviteRoleId.set($event)">
@for (role of facade.roles(); track role.id) { @for (role of facade.roles(); track role.id) {
<option [value]="role.id">{{ role.name }}</option> <option [value]="role.id">{{ roleLabel(role.id) }}</option>
} }
</select> </select>
<select [attr.aria-label]="'adminUsers.scope' | translate" [ngModel]="inviteScope()" (ngModelChange)="inviteScope.set($event)"> <select [attr.aria-label]="'adminUsers.scope' | translate" [ngModel]="inviteScope()" (ngModelChange)="inviteScope.set($event)">
@@ -77,7 +77,7 @@
@for (invite of facade.invitations(); track invite.id) { @for (invite of facade.invitations(); track invite.id) {
<tr> <tr>
<th scope="row">{{ invite.email }}</th> <th scope="row">{{ invite.email }}</th>
<td>{{ facade.roleName(invite.roleId) }}</td> <td>{{ roleLabel(invite.roleId) }}</td>
<td><app-badge variant="neutral">{{ ('adminUsers.invitationStatus.' + invite.status) | translate }}</app-badge></td> <td><app-badge variant="neutral">{{ ('adminUsers.invitationStatus.' + invite.status) | translate }}</app-badge></td>
<td> <td>
@if (invite.status === 'pending') { @if (invite.status === 'pending') {
@@ -96,7 +96,7 @@
<div class="card"> <div class="card">
<h2>{{ 'adminUsers.roles' | translate }}</h2> <h2>{{ 'adminUsers.roles' | translate }}</h2>
@for (role of facade.roles(); track role.id) { @for (role of facade.roles(); track role.id) {
<p><strong>{{ role.name }}</strong> — {{ role.permissions.join(', ') }}</p> <p><strong>{{ roleLabel(role.id) }}</strong> — {{ role.permissions.map(permissionLabel).join(', ') }}</p>
} }
</div> </div>
@@ -111,7 +111,7 @@
<app-dialog [open]="!!facade.auditTarget()" [titleText]="'adminTransactions.audit' | translate" size="sm" (closed)="facade.closeAudit()"> <app-dialog [open]="!!facade.auditTarget()" [titleText]="'adminTransactions.audit' | translate" size="sm" (closed)="facade.closeAudit()">
@for (entry of facade.audit(); track $index) { @for (entry of facade.audit(); track $index) {
<p>{{ entry.timestamp | date:'short' }} — {{ entry.actor }} — {{ entry.action }}</p> <p>{{ entry.timestamp | date:'short' }} — {{ ('adminUsers.actor.' + entry.actor) | translate }} — {{ auditText(entry) }}</p>
} }
</app-dialog> </app-dialog>
</section> </section>

View File

@@ -45,4 +45,31 @@ export class AdminUsersPageComponent {
} }
this.facade.setStatus(userId, next); this.facade.setStatus(userId, next);
} }
private static readonly PERMISSION_KEYS: Record<string, string> = {
'*': 'all',
'products.manage': 'productsManage',
'products.view': 'productsView',
'categories.manage': 'categoriesManage',
'orders.manage': 'ordersManage',
'orders.view': 'ordersView',
'media.manage': 'mediaManage',
};
readonly roleLabel = (roleId: string): string => this.translate.t('adminUsers.roleValue.' + roleId);
readonly permissionLabel = (code: string): string => {
const key = AdminUsersPageComponent.PERMISSION_KEYS[code];
return key ? this.translate.t('adminUsers.permission.' + key) : code;
};
auditText(entry: { eventKey: 'roleChanged' | 'statusChanged'; roleId?: string; status?: AdminUserStatus }): string {
if (entry.eventKey === 'roleChanged' && entry.roleId) {
return this.translate.t('adminUsers.audit.roleChanged', { role: this.roleLabel(entry.roleId) });
}
if (entry.eventKey === 'statusChanged' && entry.status) {
return this.translate.t('adminUsers.audit.statusChanged', { status: this.translate.t('adminUsers.statusValue.' + entry.status) });
}
return '';
}
} }

View File

@@ -89,7 +89,9 @@ export class AdminUsersLocalGateway implements AdminUsersGateway {
this.users = users.map(user => user.id === userId ? updated : user); this.users = users.map(user => user.id === userId ? updated : user);
this.audit[userId] = [ this.audit[userId] = [
...(this.audit[userId] ?? []), ...(this.audit[userId] ?? []),
{ action: roleId ? `Role changed to ${roleId}` : `Status changed to ${status}`, actor: 'admin', timestamp: new Date().toISOString() }, roleId
? { eventKey: 'roleChanged', roleId, actor: 'admin', timestamp: new Date().toISOString() }
: { eventKey: 'statusChanged', status, actor: 'admin', timestamp: new Date().toISOString() },
]; ];
return of(updated).pipe(delay(50)); return of(updated).pipe(delay(50));
} }

View File

@@ -1231,6 +1231,7 @@ export const en: Translations = {
adminModeration: { adminModeration: {
search: 'Search by customer, product, or review text…', search: 'Search by customer, product, or review text…',
selectAllRows: 'Select all reviews', selectAllRows: 'Select all reviews',
itemsCount: 'reviews',
allStatuses: 'All statuses', allStatuses: 'All statuses',
allRatings: 'All ratings', allRatings: 'All ratings',
rating: 'Rating', rating: 'Rating',
@@ -1287,6 +1288,16 @@ export const en: Translations = {
rejected: 'Rejected', rejected: 'Rejected',
spam: 'Spam', spam: 'Spam',
}, },
timelineEvent: {
submitted: 'Review submitted',
statusChanged: 'Status changed to "{{status}}"',
restored: 'Review restored',
hidden: 'Review hidden',
},
actor: {
admin: 'Admin',
customer: 'Customer',
},
targetType: { targetType: {
product: 'Product', product: 'Product',
review: 'Review', review: 'Review',
@@ -1315,6 +1326,11 @@ export const en: Translations = {
createdAt: 'Placed', createdAt: 'Placed',
items: 'Items', items: 'Items',
timeline: 'Timeline', timeline: 'Timeline',
timelineEvent: {
created: 'Order created',
statusChanged: 'Status changed to "{{status}}"',
refundRequested: 'Refund requested',
},
notes: 'Notes to customer', notes: 'Notes to customer',
internalNotes: 'Internal notes', internalNotes: 'Internal notes',
addNote: 'Add note', addNote: 'Add note',
@@ -1349,6 +1365,7 @@ export const en: Translations = {
refund_requested: 'Refund requested', refund_requested: 'Refund requested',
refunded: 'Refunded', refunded: 'Refunded',
}, },
itemsCount: 'orders',
ordersToday: 'Orders today', ordersToday: 'Orders today',
paidCount: 'Paid', paidCount: 'Paid',
customersCount: 'Customers', customersCount: 'Customers',
@@ -1360,6 +1377,7 @@ export const en: Translations = {
alertRefundRequested: 'orders have a refund request waiting', alertRefundRequested: 'orders have a refund request waiting',
}, },
adminCustomers: { adminCustomers: {
back: 'Back to customers',
search: 'Search by name, email, or phone…', search: 'Search by name, email, or phone…',
name: 'Name', name: 'Name',
email: 'Email', email: 'Email',
@@ -1749,11 +1767,34 @@ export const en: Translations = {
expired: 'Expired', expired: 'Expired',
revoked: 'Revoked', revoked: 'Revoked',
}, },
roleValue: {
owner: 'Owner',
admin: 'Admin',
editor: 'Editor',
viewer: 'Viewer',
},
permission: {
all: 'Full access to everything',
productsManage: 'Manage products',
productsView: 'View products',
categoriesManage: 'Manage categories',
ordersManage: 'Manage orders',
ordersView: 'View orders',
mediaManage: 'Manage media library',
},
actor: {
admin: 'Admin',
},
audit: {
roleChanged: 'Role changed to "{{role}}"',
statusChanged: 'Status changed to "{{status}}"',
},
}, },
adminTransactions: { adminTransactions: {
search: 'Search transactions', search: 'Search transactions',
type: 'Type', type: 'Type',
method: 'Payment method', method: 'Payment method',
itemsCount: 'transactions',
fraud: 'Fraud check', fraud: 'Fraud check',
flagged: 'Flagged', flagged: 'Flagged',
flag: 'Flag as suspicious', flag: 'Flag as suspicious',
@@ -1775,6 +1816,11 @@ export const en: Translations = {
refund: 'Refund', refund: 'Refund',
qr_payment: 'QR payment', qr_payment: 'QR payment',
}, },
methodValue: {
card: 'Card',
qr: 'QR code',
cash_on_delivery: 'Cash on delivery',
},
}, },
adminMonitoring: { adminMonitoring: {
eventsEmptyTitle: 'No events found', eventsEmptyTitle: 'No events found',

View File

@@ -1226,6 +1226,7 @@ export const hy: Translations = {
adminModeration: { adminModeration: {
search: 'Փնտրել ըստ հաճախորդի, ապրանքի կամ կարծիքի տեքստի…', search: 'Փնտրել ըստ հաճախորդի, ապրանքի կամ կարծիքի տեքստի…',
selectAllRows: 'Ընտրել բոլոր կարծիքները', selectAllRows: 'Ընտրել բոլոր կարծիքները',
itemsCount: 'կարծիք',
allStatuses: 'Բոլոր կարգավիճակները', allStatuses: 'Բոլոր կարգավիճակները',
allRatings: 'Բոլոր գնահատականները', allRatings: 'Բոլոր գնահատականները',
rating: 'Գնահատական', rating: 'Գնահատական',
@@ -1282,6 +1283,16 @@ export const hy: Translations = {
rejected: 'Մերժված', rejected: 'Մերժված',
spam: 'Սպամ', spam: 'Սպամ',
}, },
timelineEvent: {
submitted: 'Կարծիքն ուղարկվել է',
statusChanged: 'Կարգավիճակը փոխվել է՝ «{{status}}»',
restored: 'Կարծիքը վերականգնվել է',
hidden: 'Կարծիքը թաքցվել է',
},
actor: {
admin: 'Ադմինիստրատոր',
customer: 'Հաճախորդ',
},
targetType: { targetType: {
product: 'Ապրանք', product: 'Ապրանք',
review: 'Կարծիք', review: 'Կարծիք',
@@ -1310,6 +1321,11 @@ export const hy: Translations = {
createdAt: 'Ստեղծված է', createdAt: 'Ստեղծված է',
items: 'Ապրանքներ', items: 'Ապրանքներ',
timeline: 'Ժամանակագրություն', timeline: 'Ժամանակագրություն',
timelineEvent: {
created: 'Պատվերը ստեղծվել է',
statusChanged: 'Կարգավիճակը փոխվել է՝ «{{status}}»',
refundRequested: 'Հայցվել է վերադարձ',
},
notes: 'Նշումներ հաճախորդի համար', notes: 'Նշումներ հաճախորդի համար',
internalNotes: 'Ներքին նշումներ', internalNotes: 'Ներքին նշումներ',
addNote: 'Ավելացնել նշում', addNote: 'Ավելացնել նշում',
@@ -1344,6 +1360,7 @@ export const hy: Translations = {
refund_requested: 'Հայցվել է վերադարձ', refund_requested: 'Հայցվել է վերադարձ',
refunded: 'Վերադարձված', refunded: 'Վերադարձված',
}, },
itemsCount: 'պատվեր',
ordersToday: 'Այսօրվա պատվերներ', ordersToday: 'Այսօրվա պատվերներ',
paidCount: 'Վճարված', paidCount: 'Վճարված',
customersCount: 'Հաճախորդներ', customersCount: 'Հաճախորդներ',
@@ -1355,6 +1372,7 @@ export const hy: Translations = {
alertRefundRequested: 'պատվեր սպասում է վերադարձի հայցի', alertRefundRequested: 'պատվեր սպասում է վերադարձի հայցի',
}, },
adminCustomers: { adminCustomers: {
back: 'Վերադառնալ հաճախորդներին',
search: 'Փնտրել ըստ անվան, էլ. փոստի կամ հեռախոսի…', search: 'Փնտրել ըստ անվան, էլ. փոստի կամ հեռախոսի…',
name: 'Անուն', name: 'Անուն',
email: 'Էլ. փոստ', email: 'Էլ. փոստ',
@@ -1744,11 +1762,34 @@ export const hy: Translations = {
expired: 'Ժամկետանց', expired: 'Ժամկետանց',
revoked: 'Չեղարկված', revoked: 'Չեղարկված',
}, },
roleValue: {
owner: 'Սեփականատեր',
admin: 'Ադմինիստրատոր',
editor: 'Խմբագիր',
viewer: 'Դիտող',
},
permission: {
all: 'Ամբողջական հասանելիություն',
productsManage: 'Ապրանքների կառավարում',
productsView: 'Ապրանքների դիտում',
categoriesManage: 'Կատեգորիաների կառավարում',
ordersManage: 'Պատվերների կառավարում',
ordersView: 'Պատվերների դիտում',
mediaManage: 'Մեդիադարանի կառավարում',
},
actor: {
admin: 'Ադմինիստրատոր',
},
audit: {
roleChanged: 'Դերը փոխվել է՝ «{{role}}»',
statusChanged: 'Կարգավիճակը փոխվել է՝ «{{status}}»',
},
}, },
adminTransactions: { adminTransactions: {
search: 'Գործարքների որոնում', search: 'Գործարքների որոնում',
type: 'Տեսակ', type: 'Տեսակ',
method: 'Վճարման եղանակ', method: 'Վճարման եղանակ',
itemsCount: 'գործարք',
fraud: 'Խարդախության ստուգում', fraud: 'Խարդախության ստուգում',
flagged: 'Նշված', flagged: 'Նշված',
flag: 'Նշել որպես կասկածելի', flag: 'Նշել որպես կասկածելի',
@@ -1770,6 +1811,11 @@ export const hy: Translations = {
refund: 'Վերադարձ', refund: 'Վերադարձ',
qr_payment: 'QR վճարում', qr_payment: 'QR վճարում',
}, },
methodValue: {
card: 'Քարտ',
qr: 'QR կոդ',
cash_on_delivery: 'Կանխիկ առաքման ժամանակ',
},
}, },
adminMonitoring: { adminMonitoring: {
eventsEmptyTitle: 'Իրադարձություններ չեն գտնվել', eventsEmptyTitle: 'Իրադարձություններ չեն գտնվել',

View File

@@ -1226,6 +1226,7 @@ export const ru: Translations = {
adminModeration: { adminModeration: {
search: 'Поиск по клиенту, товару или тексту отзыва…', search: 'Поиск по клиенту, товару или тексту отзыва…',
selectAllRows: 'Выбрать все отзывы', selectAllRows: 'Выбрать все отзывы',
itemsCount: 'отзывов',
allStatuses: 'Все статусы', allStatuses: 'Все статусы',
allRatings: 'Все оценки', allRatings: 'Все оценки',
rating: 'Оценка', rating: 'Оценка',
@@ -1282,6 +1283,16 @@ export const ru: Translations = {
rejected: 'Отклонён', rejected: 'Отклонён',
spam: 'Спам', spam: 'Спам',
}, },
timelineEvent: {
submitted: 'Отзыв отправлен',
statusChanged: 'Статус изменён на «{{status}}»',
restored: 'Отзыв восстановлен',
hidden: 'Отзыв скрыт',
},
actor: {
admin: 'Администратор',
customer: 'Клиент',
},
targetType: { targetType: {
product: 'Товар', product: 'Товар',
review: 'Отзыв', review: 'Отзыв',
@@ -1310,6 +1321,11 @@ export const ru: Translations = {
createdAt: 'Оформлен', createdAt: 'Оформлен',
items: 'Товары', items: 'Товары',
timeline: 'Хронология', timeline: 'Хронология',
timelineEvent: {
created: 'Заказ создан',
statusChanged: 'Статус изменён на «{{status}}»',
refundRequested: 'Запрошен возврат',
},
notes: 'Заметки для клиента', notes: 'Заметки для клиента',
internalNotes: 'Внутренние заметки', internalNotes: 'Внутренние заметки',
addNote: 'Добавить заметку', addNote: 'Добавить заметку',
@@ -1344,6 +1360,7 @@ export const ru: Translations = {
refund_requested: 'Запрошен возврат', refund_requested: 'Запрошен возврат',
refunded: 'Возвращён', refunded: 'Возвращён',
}, },
itemsCount: 'заказов',
ordersToday: 'Заказов сегодня', ordersToday: 'Заказов сегодня',
paidCount: 'Оплачено', paidCount: 'Оплачено',
customersCount: 'Клиенты', customersCount: 'Клиенты',
@@ -1355,6 +1372,7 @@ export const ru: Translations = {
alertRefundRequested: 'заказов ждут запроса на возврат', alertRefundRequested: 'заказов ждут запроса на возврат',
}, },
adminCustomers: { adminCustomers: {
back: 'Назад к клиентам',
search: 'Поиск по имени, email или телефону…', search: 'Поиск по имени, email или телефону…',
name: 'Имя', name: 'Имя',
email: 'Email', email: 'Email',
@@ -1744,11 +1762,34 @@ export const ru: Translations = {
expired: 'Истекло', expired: 'Истекло',
revoked: 'Отозвано', revoked: 'Отозвано',
}, },
roleValue: {
owner: 'Владелец',
admin: 'Администратор',
editor: 'Редактор',
viewer: 'Наблюдатель',
},
permission: {
all: 'Полный доступ ко всем разделам',
productsManage: 'Управление товарами',
productsView: 'Просмотр товаров',
categoriesManage: 'Управление категориями',
ordersManage: 'Управление заказами',
ordersView: 'Просмотр заказов',
mediaManage: 'Управление медиатекой',
},
actor: {
admin: 'Администратор',
},
audit: {
roleChanged: 'Роль изменена на «{{role}}»',
statusChanged: 'Статус изменён на «{{status}}»',
},
}, },
adminTransactions: { adminTransactions: {
search: 'Поиск транзакций', search: 'Поиск транзакций',
type: 'Тип', type: 'Тип',
method: 'Способ оплаты', method: 'Способ оплаты',
itemsCount: 'транзакций',
fraud: 'Проверка на мошенничество', fraud: 'Проверка на мошенничество',
flagged: 'Помечено', flagged: 'Помечено',
flag: 'Пометить как подозрительную', flag: 'Пометить как подозрительную',
@@ -1770,6 +1811,11 @@ export const ru: Translations = {
refund: 'Возврат', refund: 'Возврат',
qr_payment: 'Оплата по QR', qr_payment: 'Оплата по QR',
}, },
methodValue: {
card: 'Карта',
qr: 'QR-код',
cash_on_delivery: 'Наличными при получении',
},
}, },
adminMonitoring: { adminMonitoring: {
eventsEmptyTitle: 'События не найдены', eventsEmptyTitle: 'События не найдены',

View File

@@ -1230,6 +1230,7 @@ export interface Translations {
adminModeration: { adminModeration: {
search: string; search: string;
selectAllRows: string; selectAllRows: string;
itemsCount: string;
allStatuses: string; allStatuses: string;
allRatings: string; allRatings: string;
rating: string; rating: string;
@@ -1286,6 +1287,16 @@ export interface Translations {
rejected: string; rejected: string;
spam: string; spam: string;
}; };
timelineEvent: {
submitted: string;
statusChanged: string;
restored: string;
hidden: string;
};
actor: {
admin: string;
customer: string;
};
targetType: { targetType: {
product: string; product: string;
review: string; review: string;
@@ -1314,6 +1325,11 @@ export interface Translations {
createdAt: string; createdAt: string;
items: string; items: string;
timeline: string; timeline: string;
timelineEvent: {
created: string;
statusChanged: string;
refundRequested: string;
};
notes: string; notes: string;
internalNotes: string; internalNotes: string;
addNote: string; addNote: string;
@@ -1348,6 +1364,7 @@ export interface Translations {
refund_requested: string; refund_requested: string;
refunded: string; refunded: string;
}; };
itemsCount: string;
ordersToday: string; ordersToday: string;
paidCount: string; paidCount: string;
customersCount: string; customersCount: string;
@@ -1359,6 +1376,7 @@ export interface Translations {
alertRefundRequested: string; alertRefundRequested: string;
}; };
adminCustomers: { adminCustomers: {
back: string;
search: string; search: string;
name: string; name: string;
email: string; email: string;
@@ -1757,11 +1775,34 @@ export interface Translations {
expired: string; expired: string;
revoked: string; revoked: string;
}; };
roleValue: {
owner: string;
admin: string;
editor: string;
viewer: string;
};
permission: {
all: string;
productsManage: string;
productsView: string;
categoriesManage: string;
ordersManage: string;
ordersView: string;
mediaManage: string;
};
actor: {
admin: string;
};
audit: {
roleChanged: string;
statusChanged: string;
};
}; };
adminTransactions: { adminTransactions: {
search: string; search: string;
type: string; type: string;
method: string; method: string;
itemsCount: string;
fraud: string; fraud: string;
flagged: string; flagged: string;
flag: string; flag: string;
@@ -1783,6 +1824,11 @@ export interface Translations {
refund: string; refund: string;
qr_payment: string; qr_payment: string;
}; };
methodValue: {
card: string;
qr: string;
cash_on_delivery: string;
};
}; };
adminMonitoring: { adminMonitoring: {
eventsEmptyTitle: string; eventsEmptyTitle: string;