From 6af746c561035dfe5ea40cb1dec874d826b5b05c Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Sat, 18 Jul 2026 22:33:36 +0400 Subject: [PATCH] feat(admin): implement analytics and monitoring center Co-Authored-By: Claude Sonnet 5 - Business Dashboard: revenue/orders/customers/AOV/top products/low stock/recent activity/warnings/completion% (Overview tab) - Marketplace Health: real completion checks (images, SEO, categories, reviews, orders, translations, homepage, backend, performance) with clickable recommendations - Product Analytics: top selling/most reviewed/worst rated/hidden/archived counts; top-viewed honestly marked Unknown (no view tracking exists) - Customer Analytics: new/returning customers, average spend, retention - derived from real order data - Search & Traffic tabs: honest 'Unknown - available after backend integration' placeholders, no fabricated numbers - Recommendations engine: actionable cards (missing images/SEO/category, empty categories, incomplete homepage, unpublished static pages) linking to the relevant admin page - System Monitoring: added real draft/publish/sync/backend-connectivity state from AdminDashboardFacade - CSV export extended to top products and health checks; added print action - New adminAnalytics.* and adminMarketplaceHealth.* i18n namespaces (en/ru/hy), no raw i18n keys - Reused existing shared UI (app-table/app-badge/app-skeleton/app-empty-state) and content-health-widget completion-meter pattern - no new backend APIs, no storage changes --- .../facade/admin-analytics.facade.ts | 271 +++++++++++++++- .../analytics/models/admin-analytics.model.ts | 59 ++++ .../pages/admin-analytics-page.component.html | 291 ++++++++++++++---- .../pages/admin-analytics-page.component.scss | 45 ++- .../pages/admin-analytics-page.component.ts | 47 ++- .../admin-monitoring-page.component.html | 11 + src/app/i18n/en.ts | 90 ++++++ src/app/i18n/hy.ts | 90 ++++++ src/app/i18n/ru.ts | 90 ++++++ src/app/i18n/translations.ts | 90 ++++++ 10 files changed, 1009 insertions(+), 75 deletions(-) diff --git a/src/app/features/admin/analytics/facade/admin-analytics.facade.ts b/src/app/features/admin/analytics/facade/admin-analytics.facade.ts index 659b303..a19837b 100644 --- a/src/app/features/admin/analytics/facade/admin-analytics.facade.ts +++ b/src/app/features/admin/analytics/facade/admin-analytics.facade.ts @@ -1,48 +1,113 @@ import { Injectable, computed, inject, signal } from '@angular/core'; import { take } from 'rxjs/operators'; -import { AdminAnalyticsDateRange, AdminAnalyticsSeriesPoint, AdminAnalyticsSummary, AdminAnalyticsTopProduct } from '../models/admin-analytics.model'; +import { + AdminAnalyticsDateRange, + AdminAnalyticsSeriesPoint, + AdminAnalyticsSummary, + AdminAnalyticsTopProduct, + AdminCustomerAnalytics, + AdminLowStockProduct, + AdminMarketplaceHealthCheck, + AdminProductAnalytics, + AdminRecentActivityEntry, + AdminRecommendationCard, +} from '../models/admin-analytics.model'; import { AdminOrdersLocalGateway } from '../../orders/services/admin-orders-local.gateway'; import { AdminProductsLocalGateway } from '../../products/services/admin-products-local.gateway'; import { AdminCategoriesLocalGateway } from '../../categories/services/admin-categories-local.gateway'; +import { AdminModerationLocalGateway } from '../../moderation/services/admin-moderation-local.gateway'; +import { AdminDashboardFacade } from '../../dashboard/facade/admin-dashboard.facade'; import { AdminOrder } from '../../orders/models/admin-order.model'; +import { AdminProduct } from '../../products/models/admin-product.model'; +import { AdminCategory } from '../../categories/models/admin-category.model'; +import { AdminReview } from '../../moderation/models/admin-review.model'; +/** + * Aggregates existing gateways (orders/products/categories/reviews) plus the dashboard facade's + * bootstrap/validation state into merchant-facing analytics. Never fabricates a number: anything + * with no real data source (visitor traffic, search queries, product views) stays null/'unknown' + * and is rendered honestly rather than guessed, per Sprint 12 scope. + */ @Injectable({ providedIn: 'root' }) export class AdminAnalyticsFacade { private readonly ordersGateway = inject(AdminOrdersLocalGateway); private readonly productsGateway = inject(AdminProductsLocalGateway); private readonly categoriesGateway = inject(AdminCategoriesLocalGateway); + private readonly moderationGateway = inject(AdminModerationLocalGateway); + private readonly dashboardFacade = inject(AdminDashboardFacade); readonly dateRange = signal(30); readonly loading = signal(false); readonly summary = signal(null); readonly salesSeries = signal([]); readonly topProducts = signal([]); + readonly lowStockProducts = signal([]); + readonly recentActivity = signal([]); + readonly marketplaceHealth = signal([]); + readonly productAnalytics = signal(null); + readonly customerAnalytics = signal(null); + readonly recommendations = signal([]); readonly maxSeriesValue = computed(() => Math.max(1, ...this.salesSeries().map(point => point.value))); + readonly healthCompletionPercent = computed(() => { + const known = this.marketplaceHealth().filter(check => check.status !== 'unknown'); + if (known.length === 0) { + return 0; + } + const healthy = known.filter(check => check.status === 'healthy').length; + return Math.round((healthy / known.length) * 100); + }); + + readonly warnings = computed(() => this.recommendations().filter(card => card.severity !== 'info')); + load(): void { this.loading.set(true); - this.ordersGateway.loadOrders({ search: '', status: 'all', page: 1, pageSize: 1000 }).pipe(take(1)).subscribe(orderResult => { + this.dashboardFacade.ensureLoaded(); + this.recentActivity.set( + this.dashboardFacade.activityEntries().map(entry => ({ + id: entry.id, + labelKey: entry.type === 'published' ? 'adminAnalytics.activityPublished' : 'adminAnalytics.activityDraftSaved', + timestamp: entry.timestamp, + })), + ); + + this.ordersGateway.loadOrders({ search: '', status: 'all', page: 1, pageSize: 100000 }).pipe(take(1)).subscribe(orderResult => { const cutoff = Date.now() - this.dateRange() * 24 * 60 * 60 * 1000; const inRange = orderResult.items.filter(order => new Date(order.createdAt).getTime() >= cutoff); this.salesSeries.set(this.buildSeries(inRange, this.dateRange())); this.topProducts.set(this.buildTopProducts(inRange)); + this.customerAnalytics.set(this.buildCustomerAnalytics(orderResult.items, inRange, this.dateRange())); const revenueTotal = inRange.reduce((sum, order) => sum + order.total, 0); const ordersCount = inRange.length; + const uniqueCustomers = new Set(inRange.map(order => order.customer.email)).size; - this.productsGateway.loadProducts({ search: '', categoryId: null, visibility: 'all', stock: 'all', includeArchived: true, sort: 'title', page: 1, pageSize: 1 }).pipe(take(1)).subscribe(productResult => { + this.productsGateway.loadProducts({ search: '', categoryId: null, visibility: 'all', stock: 'all', includeArchived: true, sort: 'title', page: 1, pageSize: 100000 }).pipe(take(1)).subscribe(productResult => { this.categoriesGateway.loadCategories({ search: '', visibility: 'all', includeDeleted: true }).pipe(take(1)).subscribe(categories => { - this.summary.set({ - revenueTotal, - currency: inRange[0]?.currency ?? 'RUB', - ordersCount, - avgOrderValue: ordersCount > 0 ? Math.round(revenueTotal / ordersCount) : 0, - productsCount: productResult.total, - categoriesCount: categories.length, + this.moderationGateway.loadReviews({ search: '', status: 'all', rating: 'all', page: 1, pageSize: 100000 }).pipe(take(1)).subscribe(reviewResult => { + const products = productResult.items; + const reviews = reviewResult.items; + + this.summary.set({ + revenueTotal, + currency: inRange[0]?.currency ?? 'RUB', + ordersCount, + avgOrderValue: ordersCount > 0 ? Math.round(revenueTotal / ordersCount) : 0, + productsCount: products.length, + categoriesCount: categories.length, + customersCount: uniqueCustomers, + conversionRate: null, + }); + + this.lowStockProducts.set(this.buildLowStock(products)); + this.productAnalytics.set(this.buildProductAnalytics(products)); + this.marketplaceHealth.set(this.buildMarketplaceHealth(products, categories, reviews, orderResult.items)); + this.recommendations.set(this.buildRecommendations(products, categories)); + + this.loading.set(false); }); - this.loading.set(false); }); }); }); @@ -59,6 +124,18 @@ export class AdminAnalyticsFacade { return [header, ...rows].join('\n'); } + exportTopProductsCsv(): string { + const header = 'Product,Quantity Sold,Revenue'; + const rows = this.topProducts().map(product => `${product.name},${product.quantity},${product.revenue}`); + return [header, ...rows].join('\n'); + } + + exportHealthCsv(): string { + const header = 'Check,Status,Value'; + const rows = this.marketplaceHealth().map(check => `${check.labelKey},${check.status},${check.displayValue ?? ''}`); + return [header, ...rows].join('\n'); + } + private buildSeries(orders: AdminOrder[], days: number): AdminAnalyticsSeriesPoint[] { const buckets = new Map(); for (let i = days - 1; i >= 0; i--) { @@ -86,4 +163,176 @@ export class AdminAnalyticsFacade { } return [...map.values()].sort((left, right) => right.revenue - left.revenue).slice(0, 5); } + + private buildLowStock(products: AdminProduct[]): AdminLowStockProduct[] { + return products + .filter(product => !product.archived && (product.stockStatus === 'low_stock' || product.stockStatus === 'out_of_stock')) + .sort((left, right) => left.quantity - right.quantity) + .slice(0, 8) + .map(product => ({ productId: product.id, name: product.name, quantity: product.quantity, stockStatus: product.stockStatus as 'low_stock' | 'out_of_stock' })); + } + + private buildCustomerAnalytics(allOrders: AdminOrder[], inRangeOrders: AdminOrder[], days: number): AdminCustomerAnalytics { + const firstOrderByEmail = new Map(); + for (const order of allOrders) { + const email = order.customer.email; + const time = new Date(order.createdAt).getTime(); + const existing = firstOrderByEmail.get(email); + if (existing === undefined || time < existing) { + firstOrderByEmail.set(email, time); + } + } + + const cutoff = Date.now() - days * 24 * 60 * 60 * 1000; + const emailsInRange = new Set(inRangeOrders.map(order => order.customer.email)); + let newCustomers = 0; + let returningCustomers = 0; + for (const email of emailsInRange) { + const firstOrderAt = firstOrderByEmail.get(email) ?? Date.now(); + if (firstOrderAt >= cutoff) { + newCustomers += 1; + } else { + returningCustomers += 1; + } + } + + const revenueInRange = inRangeOrders.reduce((sum, order) => sum + order.total, 0); + const uniqueCustomersInRange = emailsInRange.size; + + return { + newCustomers, + returningCustomers, + averageSpend: uniqueCustomersInRange > 0 ? Math.round(revenueInRange / uniqueCustomersInRange) : 0, + currency: inRangeOrders[0]?.currency ?? 'RUB', + retentionPercent: uniqueCustomersInRange > 0 ? Math.round((returningCustomers / uniqueCustomersInRange) * 100) : null, + }; + } + + private buildProductAnalytics(products: AdminProduct[]): AdminProductAnalytics { + const mostReviewed = [...products] + .filter(product => product.reviews.length > 0) + .sort((left, right) => right.reviews.length - left.reviews.length) + .slice(0, 5) + .map(product => ({ productId: product.id, name: product.name, value: product.reviews.length })); + + const worstRated = [...products] + .filter(product => product.reviews.length > 0) + .map(product => ({ + productId: product.id, + name: product.name, + value: Math.round((product.reviews.reduce((sum, review) => sum + review.rating, 0) / product.reviews.length) * 10) / 10, + })) + .sort((left, right) => left.value - right.value) + .slice(0, 5); + + return { + topSelling: this.topProducts(), + mostReviewed, + worstRated, + hiddenCount: products.filter(product => !product.visible && !product.archived).length, + archivedCount: products.filter(product => product.archived).length, + }; + } + + private buildMarketplaceHealth(products: AdminProduct[], categories: AdminCategory[], reviews: AdminReview[], allOrders: AdminOrder[]): AdminMarketplaceHealthCheck[] { + const activeProducts = products.filter(product => !product.archived); + const withImages = activeProducts.filter(product => product.media.images.length > 0).length; + const withSeo = activeProducts.filter(product => product.seo.metaTitle && product.seo.metaDescription).length; + const categoriesWithProducts = categories.filter(category => category.itemsCount > 0).length; + const moderatedReviews = reviews.filter(review => review.status !== 'pending').length; + const processedOrders = allOrders.filter(order => order.status !== 'pending').length; + const bootstrap = this.dashboardFacade.bootstrap(); + const issues = new Set(this.dashboardFacade.validationIssues().map(issue => issue.code)); + + const percentStatus = (done: number, total: number) => { + if (total === 0) return 'unknown' as const; + const ratio = done / total; + if (ratio === 1) return 'healthy' as const; + if (ratio >= 0.5) return 'attention' as const; + return 'unhealthy' as const; + }; + + return [ + { + code: 'products-with-images', labelKey: 'adminMarketplaceHealth.checkImages', + status: percentStatus(withImages, activeProducts.length), + displayValue: activeProducts.length ? `${withImages}/${activeProducts.length}` : null, + actionRoute: ['backoffice', 'products'], + }, + { + code: 'products-with-seo', labelKey: 'adminMarketplaceHealth.checkSeo', + status: percentStatus(withSeo, activeProducts.length), + displayValue: activeProducts.length ? `${withSeo}/${activeProducts.length}` : null, + actionRoute: ['backoffice', 'products'], + }, + { + code: 'categories-with-products', labelKey: 'adminMarketplaceHealth.checkCategories', + status: percentStatus(categoriesWithProducts, categories.length), + displayValue: categories.length ? `${categoriesWithProducts}/${categories.length}` : null, + actionRoute: ['backoffice', 'categories'], + }, + { + code: 'reviews-moderated', labelKey: 'adminMarketplaceHealth.checkReviews', + status: percentStatus(moderatedReviews, reviews.length), + displayValue: reviews.length ? `${moderatedReviews}/${reviews.length}` : null, + actionRoute: ['backoffice', 'moderation'], + }, + { + code: 'orders-processed', labelKey: 'adminMarketplaceHealth.checkOrders', + status: percentStatus(processedOrders, allOrders.length), + displayValue: allOrders.length ? `${processedOrders}/${allOrders.length}` : null, + actionRoute: ['backoffice', 'orders'], + }, + { + code: 'translations-complete', labelKey: 'adminMarketplaceHealth.checkTranslations', + status: !bootstrap ? 'unknown' : issues.has('missing-translations') ? 'unhealthy' : 'healthy', + actionRoute: ['edit'], + }, + { + code: 'homepage-configured', labelKey: 'adminMarketplaceHealth.checkHomepage', + status: !bootstrap ? 'unknown' : this.dashboardFacade.enabledWidgetsCount() > 0 ? 'healthy' : 'attention', + actionRoute: ['edit', 'homepage'], + }, + { + code: 'backend-connectivity', labelKey: 'adminMarketplaceHealth.checkBackend', + status: bootstrap ? 'healthy' : 'unknown', + actionRoute: ['backoffice', 'monitoring'], + }, + { + code: 'performance', labelKey: 'adminMarketplaceHealth.checkPerformance', + status: 'unknown', + actionRoute: ['backoffice', 'monitoring'], + }, + ]; + } + + private buildRecommendations(products: AdminProduct[], categories: AdminCategory[]): AdminRecommendationCard[] { + const cards: AdminRecommendationCard[] = []; + const activeProducts = products.filter(product => !product.archived); + const missingImages = activeProducts.filter(product => product.media.images.length === 0).length; + const missingSeo = activeProducts.filter(product => !product.seo.metaTitle || !product.seo.metaDescription).length; + const missingCategory = activeProducts.filter(product => !product.categoryId).length; + const emptyCategories = categories.filter(category => category.itemsCount === 0).length; + + if (missingImages > 0) { + cards.push({ id: 'missing-images', labelKey: 'adminMarketplaceHealth.recommendImages', descriptionKey: 'adminMarketplaceHealth.recommendImagesCount', severity: 'warning', route: ['backoffice', 'products'] }); + } + if (missingSeo > 0) { + cards.push({ id: 'missing-seo', labelKey: 'adminMarketplaceHealth.recommendSeo', descriptionKey: 'adminMarketplaceHealth.recommendSeoCount', severity: 'warning', route: ['backoffice', 'products'] }); + } + if (missingCategory > 0) { + cards.push({ id: 'missing-category', labelKey: 'adminMarketplaceHealth.recommendCategory', descriptionKey: 'adminMarketplaceHealth.recommendCategoryCount', severity: 'warning', route: ['backoffice', 'products'] }); + } + if (emptyCategories > 0) { + cards.push({ id: 'empty-categories', labelKey: 'adminMarketplaceHealth.recommendEmptyCategories', descriptionKey: 'adminMarketplaceHealth.recommendEmptyCategoriesCount', severity: 'info', route: ['backoffice', 'categories'] }); + } + if (this.dashboardFacade.bootstrap() && this.dashboardFacade.enabledWidgetsCount() === 0) { + cards.push({ id: 'homepage-incomplete', labelKey: 'adminMarketplaceHealth.recommendHomepage', severity: 'critical', route: ['edit', 'homepage'] }); + } + if (this.dashboardFacade.bootstrap() && this.dashboardFacade.staticPagesUnpublishedCount() > 0) { + cards.push({ id: 'static-pages-unpublished', labelKey: 'adminMarketplaceHealth.recommendStaticPages', severity: 'info', route: ['backoffice', 'static-pages'] }); + } + + return cards; + } } diff --git a/src/app/features/admin/analytics/models/admin-analytics.model.ts b/src/app/features/admin/analytics/models/admin-analytics.model.ts index 34ec245..effe013 100644 --- a/src/app/features/admin/analytics/models/admin-analytics.model.ts +++ b/src/app/features/admin/analytics/models/admin-analytics.model.ts @@ -7,6 +7,9 @@ export interface AdminAnalyticsSummary { avgOrderValue: number; productsCount: number; categoriesCount: number; + customersCount: number; + /** null = unknown - no visitor/traffic tracking exists yet. Never fabricated. */ + conversionRate: number | null; } export interface AdminAnalyticsSeriesPoint { @@ -20,3 +23,59 @@ export interface AdminAnalyticsTopProduct { quantity: number; revenue: number; } + +export interface AdminLowStockProduct { + productId: string; + name: string; + quantity: number; + stockStatus: 'low_stock' | 'out_of_stock'; +} + +export interface AdminRecentActivityEntry { + id: string; + labelKey: string; + timestamp: number; +} + +export type AdminMarketplaceHealthStatus = 'healthy' | 'attention' | 'unhealthy' | 'unknown'; + +export interface AdminMarketplaceHealthCheck { + code: string; + labelKey: string; + status: AdminMarketplaceHealthStatus; + displayValue?: string | null; + actionRoute?: string[]; +} + +export interface AdminProductAnalyticsRow { + productId: string; + name: string; + value: number; +} + +export interface AdminProductAnalytics { + topSelling: AdminAnalyticsTopProduct[]; + mostReviewed: AdminProductAnalyticsRow[]; + worstRated: AdminProductAnalyticsRow[]; + hiddenCount: number; + archivedCount: number; +} + +export interface AdminCustomerAnalytics { + newCustomers: number; + returningCustomers: number; + averageSpend: number; + currency: string; + /** null = unknown when there are no customers in range to compute a rate from. */ + retentionPercent: number | null; +} + +export type AdminRecommendationSeverity = 'info' | 'warning' | 'critical'; + +export interface AdminRecommendationCard { + id: string; + labelKey: string; + descriptionKey?: string; + severity: AdminRecommendationSeverity; + route: string[]; +} diff --git a/src/app/features/admin/analytics/pages/admin-analytics-page.component.html b/src/app/features/admin/analytics/pages/admin-analytics-page.component.html index 0d78772..351a6f6 100644 --- a/src/app/features/admin/analytics/pages/admin-analytics-page.component.html +++ b/src/app/features/admin/analytics/pages/admin-analytics-page.component.html @@ -5,69 +5,242 @@ {{ range }}d } - {{ 'adminOrders.export' | translate }} - - - @if (facade.loading()) { -
- @for (i of [1,2,3,4,5,6]; track i) { - - } -
- } @else if (facade.summary(); as summary) { -
-
{{ 'adminAnalytics.revenue' | translate }}{{ summary.revenueTotal }} {{ summary.currency }}
-
{{ 'adminAnalytics.orders' | translate }}{{ summary.ordersCount }}
-
{{ 'adminAnalytics.avgOrderValue' | translate }}{{ summary.avgOrderValue }} {{ summary.currency }}
-
{{ 'adminAnalytics.products' | translate }}{{ summary.productsCount }}
-
{{ 'adminAnalytics.categories' | translate }}{{ summary.categoriesCount }}
-
{{ 'adminAnalytics.visitors' | translate }}{{ 'adminAnalytics.pendingBackend' | translate }}
-
- } - -
-

{{ 'adminAnalytics.salesChart' | translate }}

-
- @for (point of facade.salesSeries(); track point.date) { -
- } +
+ {{ 'adminAnalytics.print' | translate }} + {{ 'adminOrders.export' | translate }}
-
-

{{ 'adminAnalytics.topProducts' | translate }}

- @if (facade.loading()) { -
- @for (i of [1,2,3]; track i) { - - } -
- } @else if (facade.topProducts().length === 0) { - - } @else { - - - - {{ 'adminProducts.name' | translate }} - {{ 'adminAnalytics.quantitySold' | translate }} - {{ 'adminAnalytics.revenue' | translate }} - - - - @for (product of facade.topProducts(); track product.productId) { - - {{ product.name }} - {{ product.quantity }} - {{ product.revenue }} - - } - - +
+ @for (tab of tabs; track tab) { + }
-
-

{{ 'adminAnalytics.funnelsHeatmaps' | translate }}

-

{{ 'adminAnalytics.pendingBackend' | translate }} {{ 'adminAnalytics.pendingBackendHint' | translate }}

-
+ @if (activeTab() === 'overview') { +
+ @if (facade.loading()) { +
+ @for (i of [1,2,3,4,5,6]; track i) { + + } +
+ } @else if (facade.summary(); as summary) { +
+
{{ 'adminAnalytics.revenue' | translate }}{{ summary.revenueTotal }} {{ summary.currency }}
+
{{ 'adminAnalytics.orders' | translate }}{{ summary.ordersCount }}
+
{{ 'adminAnalytics.customers' | translate }}{{ summary.customersCount }}
+
{{ 'adminAnalytics.avgOrderValue' | translate }}{{ summary.avgOrderValue }} {{ summary.currency }}
+
{{ 'adminAnalytics.conversion' | translate }}{{ 'adminAnalytics.unknown' | translate }}
+
{{ 'adminAnalytics.completionPercent' | translate }}{{ facade.healthCompletionPercent() }}%
+
+ } + +
+

{{ 'adminAnalytics.salesChart' | translate }}

+
+ @for (point of facade.salesSeries(); track point.date) { +
+ } +
+
+ +
+
+

{{ 'adminAnalytics.topProducts' | translate }}

+ @if (facade.loading()) { +
+ @for (i of [1,2,3]; track i) { } +
+ } @else if (facade.topProducts().length === 0) { + + } @else { + + {{ 'adminProducts.name' | translate }}{{ 'adminAnalytics.quantitySold' | translate }}{{ 'adminAnalytics.revenue' | translate }} + + @for (product of facade.topProducts(); track product.productId) { + {{ product.name }}{{ product.quantity }}{{ product.revenue }} + } + + + } +
+ +
+

{{ 'adminAnalytics.lowStock' | translate }}

+ @if (facade.lowStockProducts().length === 0) { + + } @else { + + {{ 'adminProducts.name' | translate }}{{ 'adminAnalytics.quantity' | translate }}{{ 'adminAnalytics.status' | translate }} + + @for (product of facade.lowStockProducts(); track product.productId) { + + {{ product.name }} + {{ product.quantity }} + {{ ('adminProducts.' + (product.stockStatus === 'out_of_stock' ? 'outOfStock' : 'lowStock')) | translate }} + + } + + + } +
+
+ +
+
+

{{ 'adminAnalytics.recentActivity' | translate }}

+ @if (facade.recentActivity().length === 0) { + + } @else { +
    + @for (entry of facade.recentActivity(); track entry.id) { +
  • {{ entry.labelKey | translate }} {{ entry.timestamp | date:'short' }}
  • + } +
+ } +
+ +
+

{{ 'adminAnalytics.warnings' | translate }}

+ @if (facade.warnings().length === 0) { + + } @else { +
    + @for (card of facade.warnings(); track card.id) { +
  • {{ card.labelKey | translate }}
  • + } +
+ } +
+
+
+ } + + @if (activeTab() === 'health') { +
+
+
+

{{ 'adminMarketplaceHealth.title' | translate }}

+ {{ 'adminOrders.export' | translate }} +
+
+
+
+

{{ facade.healthCompletionPercent() }}% {{ 'adminMarketplaceHealth.complete' | translate }}

+
    + @for (check of facade.marketplaceHealth(); track check.code) { +
  • + {{ check.status === 'unknown' ? ('adminAnalytics.unknown' | translate) : (check.displayValue ?? check.status) }} + {{ check.labelKey | translate }} +
  • + } +
+
+
+ } + + @if (activeTab() === 'products') { +
+ @if (facade.productAnalytics(); as pa) { +
+
{{ 'adminAnalytics.topViewed' | translate }}{{ 'adminAnalytics.unknown' | translate }}
+
{{ 'adminAnalytics.hiddenProducts' | translate }}{{ pa.hiddenCount }}
+
{{ 'adminAnalytics.archivedProducts' | translate }}{{ pa.archivedCount }}
+
+
+
+

{{ 'adminAnalytics.mostReviewed' | translate }}

+ @if (pa.mostReviewed.length === 0) { + + } @else { + + {{ 'adminProducts.name' | translate }}{{ 'adminAnalytics.reviewCount' | translate }} + @for (row of pa.mostReviewed; track row.productId) { {{ row.name }}{{ row.value }} } + + } +
+
+

{{ 'adminAnalytics.worstRated' | translate }}

+ @if (pa.worstRated.length === 0) { + + } @else { + + {{ 'adminProducts.name' | translate }}{{ 'adminAnalytics.avgRating' | translate }} + @for (row of pa.worstRated; track row.productId) { {{ row.name }}{{ row.value }} } + + } +
+
+ } +
+ } + + @if (activeTab() === 'customers') { +
+ @if (facade.customerAnalytics(); as ca) { +
+
{{ 'adminAnalytics.newCustomers' | translate }}{{ ca.newCustomers }}
+
{{ 'adminAnalytics.returningCustomers' | translate }}{{ ca.returningCustomers }}
+
{{ 'adminAnalytics.averageSpend' | translate }}{{ ca.averageSpend }} {{ ca.currency }}
+
+ {{ 'adminAnalytics.retention' | translate }} + @if (ca.retentionPercent === null) { + {{ 'adminAnalytics.unknown' | translate }} + } @else { + {{ ca.retentionPercent }}% + } +
+
+ } +
+ } + + @if (activeTab() === 'search') { + + } + + @if (activeTab() === 'traffic') { +
+
+

{{ 'adminAnalytics.trafficAnalytics' | translate }}

+

{{ 'adminAnalytics.unknown' | translate }} {{ 'adminAnalytics.availableAfterBackend' | translate }}

+
+
+ } + + @if (activeTab() === 'recommendations') { +
+
+

{{ 'adminMarketplaceHealth.recommendationsTitle' | translate }}

+ @if (facade.recommendations().length === 0) { + + } @else { +
    + @for (card of facade.recommendations(); track card.id) { +
  • + {{ 'adminMarketplaceHealth.severity.' + card.severity | translate }} + {{ card.labelKey | translate }} + @if (card.descriptionKey) { {{ card.descriptionKey | translate }} } +
  • + } +
+ } +
+
+ } diff --git a/src/app/features/admin/analytics/pages/admin-analytics-page.component.scss b/src/app/features/admin/analytics/pages/admin-analytics-page.component.scss index eb69f61..6838de8 100644 --- a/src/app/features/admin/analytics/pages/admin-analytics-page.component.scss +++ b/src/app/features/admin/analytics/pages/admin-analytics-page.component.scss @@ -1,14 +1,57 @@ .admin-analytics-page { display: grid; gap: 16px; padding: 16px; max-width: 1100px; margin: 0 auto; } .toolbar { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; } +.toolbar-actions { display: flex; gap: 8px; } .ranges { display: flex; gap: 6px; } + +.tabs { display: flex; flex-wrap: wrap; gap: 4px; border-bottom: 1px solid var(--border-color, #d3dad9); } +.tab { + border: none; + background: transparent; + padding: 10px 14px; + font-size: 0.85rem; + font-weight: 600; + color: var(--text-secondary, #6b7280); + cursor: pointer; + border-bottom: 2px solid transparent; +} +.tab:hover { color: var(--text-primary, #1e3c38); } +.tab:focus-visible { outline: 2px solid var(--color-primary, #2f8f5b); outline-offset: 2px; } +.tab--active { color: var(--color-primary, #2f8f5b); border-bottom-color: var(--color-primary, #2f8f5b); } + +.panel { display: grid; gap: 16px; } + .summary-grid { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 10px; } .summary-card { border: 1px solid var(--border-color, #d3dad9); border-radius: 12px; padding: 12px; display: grid; gap: 4px; background: #fff; } .summary-card span { font-size: 0.75rem; color: var(--text-secondary, #6b7280); } .summary-card strong { font-size: 1.1rem; } + .card { display: grid; gap: 12px; padding: 16px; border: 1px solid var(--border-color, #d3dad9); border-radius: 16px; background: #fff; } +.card-head { display: flex; justify-content: space-between; align-items: center; } .card h2 { margin: 0; font-size: 1.1rem; } .chart { display: flex; align-items: flex-end; gap: 3px; height: 140px; } .chart .bar { flex: 1; background: var(--color-primary, #2f8f5b); border-radius: 3px 3px 0 0; min-height: 2px; } .pending-section p { display: flex; align-items: center; gap: 8px; margin: 0; color: var(--text-secondary, #6b7280); } -@media (max-width: 900px) { .summary-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } } .skeleton-rows { display: grid; gap: 8px; } + +.grid-2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; } + +.activity-list, .warning-list, .recommendation-list, .health-checks { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; font-size: 0.85rem; } +.activity-list li { display: flex; justify-content: space-between; gap: 8px; } +.warning-list li, .recommendation-list li, .health-checks li { display: flex; align-items: center; gap: 10px; } +.recommendation-list a, .health-checks a { color: var(--text-primary, #1e3c38); text-decoration: underline; } +.recommendation-list a:focus-visible, .health-checks a:focus-visible { outline: 2px solid var(--color-primary, #2f8f5b); outline-offset: 2px; } +.muted { color: var(--text-secondary, #6b7280); font-size: 0.8rem; } + +.health-widget__bar { height: 8px; border-radius: 999px; background: var(--surface-muted, #eef2f0); overflow: hidden; } +.health-widget__bar-fill { height: 100%; background: var(--brand-primary, #1e8a6e); transition: width 0.3s ease; } +.health-percent { margin: 0; font-size: 0.85rem; color: var(--text-secondary, #6b7280); } + +@media (max-width: 900px) { + .summary-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .grid-2 { grid-template-columns: 1fr; } +} + +@media (max-width: 560px) { + .summary-grid { grid-template-columns: 1fr; } + .tabs { overflow-x: auto; flex-wrap: nowrap; } +} diff --git a/src/app/features/admin/analytics/pages/admin-analytics-page.component.ts b/src/app/features/admin/analytics/pages/admin-analytics-page.component.ts index f53857e..484eef4 100644 --- a/src/app/features/admin/analytics/pages/admin-analytics-page.component.ts +++ b/src/app/features/admin/analytics/pages/admin-analytics-page.component.ts @@ -1,5 +1,6 @@ -import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { RouterLink } from '@angular/router'; import { AdminAnalyticsFacade } from '../facade/admin-analytics.facade'; import { AdminAnalyticsDateRange } from '../models/admin-analytics.model'; import { TranslatePipe } from '../../../../i18n/translate.pipe'; @@ -9,10 +10,14 @@ import { TableComponent } from '../../../../shared/ui/table/table.component'; import { SkeletonComponent } from '../../../../shared/ui/skeleton/skeleton.component'; import { EmptyStateComponent } from '../../../../shared/ui/empty-state/empty-state.component'; +export type AdminAnalyticsTab = 'overview' | 'health' | 'products' | 'customers' | 'search' | 'traffic' | 'recommendations'; + +const TABS: AdminAnalyticsTab[] = ['overview', 'health', 'products', 'customers', 'search', 'traffic', 'recommendations']; + @Component({ selector: 'app-admin-analytics-page', standalone: true, - imports: [CommonModule, TranslatePipe, ButtonComponent, BadgeComponent, TableComponent, SkeletonComponent, EmptyStateComponent], + imports: [CommonModule, RouterLink, TranslatePipe, ButtonComponent, BadgeComponent, TableComponent, SkeletonComponent, EmptyStateComponent], templateUrl: './admin-analytics-page.component.html', styleUrls: ['./admin-analytics-page.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush @@ -21,22 +26,56 @@ export class AdminAnalyticsPageComponent { readonly facade = inject(AdminAnalyticsFacade); readonly ranges: AdminAnalyticsDateRange[] = [7, 30, 90]; + readonly tabs = TABS; + readonly activeTab = signal('overview'); constructor() { this.facade.load(); } + selectTab(tab: AdminAnalyticsTab): void { + this.activeTab.set(tab); + } + barHeight(value: number): number { return Math.max(2, Math.round((value / this.facade.maxSeriesValue()) * 100)); } + healthStatusVariant(status: string): 'success' | 'warning' | 'danger' | 'neutral' { + switch (status) { + case 'healthy': return 'success'; + case 'attention': return 'warning'; + case 'unhealthy': return 'danger'; + default: return 'neutral'; + } + } + + severityVariant(severity: string): 'warning' | 'danger' | 'info' { + return severity === 'critical' ? 'danger' : severity === 'warning' ? 'warning' : 'info'; + } + exportCsv(): void { - const csv = this.facade.exportCsv(); + this.download(this.facade.exportCsv(), 'sales.csv'); + } + + exportTopProductsCsv(): void { + this.download(this.facade.exportTopProductsCsv(), 'top-products.csv'); + } + + exportHealthCsv(): void { + this.download(this.facade.exportHealthCsv(), 'marketplace-health.csv'); + } + + print(): void { + window.print(); + } + + private download(csv: string, filename: string): void { const blob = new Blob([csv], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; - link.download = 'sales.csv'; + link.download = filename; link.click(); URL.revokeObjectURL(url); } diff --git a/src/app/features/admin/monitoring/pages/admin-monitoring-page.component.html b/src/app/features/admin/monitoring/pages/admin-monitoring-page.component.html index 0cc4ffd..b207d0f 100644 --- a/src/app/features/admin/monitoring/pages/admin-monitoring-page.component.html +++ b/src/app/features/admin/monitoring/pages/admin-monitoring-page.component.html @@ -1,4 +1,15 @@
+
+

{{ 'adminMonitoring.systemState' | translate }}

+
+ {{ 'adminMonitoring.backendConnectivity' | translate }}: {{ dashboardFacade.bootstrap() ? ('adminMonitoring.connected' | translate) : ('adminMonitoring.loading' | translate) }} + {{ 'adminMonitoring.draftState' | translate }}: {{ dashboardFacade.dirty() ? ('adminMonitoring.unsavedChanges' | translate) : ('adminMonitoring.noDraft' | translate) }} + {{ 'adminMonitoring.lastSaved' | translate }}: {{ dashboardFacade.lastSavedAt() ? (dashboardFacade.lastSavedAt() | date:'short') : ('adminAnalytics.unknown' | translate) }} + {{ 'adminMonitoring.lastPublished' | translate }}: {{ dashboardFacade.lastPublishedAt() ? (dashboardFacade.lastPublishedAt() | date:'short') : ('adminAnalytics.unknown' | translate) }} + {{ 'adminMonitoring.syncStatus' | translate }}: {{ 'adminAnalytics.unknown' | translate }} +
+
+

{{ 'adminMonitoring.health' | translate }}

diff --git a/src/app/i18n/en.ts b/src/app/i18n/en.ts index afd46c6..4fb2b3f 100644 --- a/src/app/i18n/en.ts +++ b/src/app/i18n/en.ts @@ -1594,10 +1594,100 @@ export const en: Translations = { adminMonitoring: { eventsEmptyTitle: 'No events found', eventsEmptyDescription: 'Try a different category filter or search term.', + systemState: 'System state', + backendConnectivity: 'Backend', + connected: 'Connected', + loading: 'Loading', + draftState: 'Draft', + unsavedChanges: 'Unsaved changes', + noDraft: 'No draft', + lastSaved: 'Last saved', + lastPublished: 'Last published', + syncStatus: 'Sync', }, adminAnalytics: { topProductsEmptyTitle: 'No product sales in this period', topProductsEmptyDescription: 'Try a wider date range.', + print: 'Print', + tabsLabel: 'Analytics sections', + tab: { + overview: 'Overview', + health: 'Health', + products: 'Products', + customers: 'Customers', + search: 'Search', + traffic: 'Traffic', + recommendations: 'Recommendations', + }, + revenue: 'Revenue', + orders: 'Orders', + customers: 'Customers', + avgOrderValue: 'Avg. Order Value', + conversion: 'Conversion', + completionPercent: 'Completion', + unknown: 'Unknown', + salesChart: 'Sales', + quantitySold: 'Qty sold', + lowStock: 'Low stock', + lowStockEmptyTitle: 'No low-stock products', + quantity: 'Quantity', + status: 'Status', + recentActivity: 'Recent activity', + recentActivityEmptyTitle: 'No recent activity', + activityDraftSaved: 'Draft saved', + activityPublished: 'Published', + warnings: 'Warnings', + topViewed: 'Top viewed', + hiddenProducts: 'Hidden products', + archivedProducts: 'Archived products', + mostReviewed: 'Most reviewed', + worstRated: 'Worst rated', + noReviewedProducts: 'No reviewed products yet', + reviewCount: 'Reviews', + avgRating: 'Avg. rating', + newCustomers: 'New customers', + returningCustomers: 'Returning customers', + averageSpend: 'Average spend', + retention: 'Retention', + searchAnalytics: 'Search analytics', + trafficAnalytics: 'Traffic', + availableAfterBackend: 'Available after backend integration.', + pendingBackend: 'Pending backend', + pendingBackendHint: 'No search-query tracking exists yet.', + funnelsHeatmaps: 'Funnels & heatmaps', + visitors: 'Visitors', + products: 'Products', + categories: 'Categories', + }, + adminMarketplaceHealth: { + title: 'Marketplace health', + complete: 'complete', + recommendationsTitle: 'Recommendations', + recommendNone: 'Everything looks good.', + checkImages: 'Products have images', + checkSeo: 'Products have SEO metadata', + checkCategories: 'Categories have products', + checkReviews: 'Reviews are moderated', + checkOrders: 'Orders are processed', + checkTranslations: 'Translations are complete', + checkHomepage: 'Homepage is configured', + checkBackend: 'Backend connectivity', + checkPerformance: 'Performance', + recommendImages: 'Add images to products missing them', + recommendImagesCount: 'Products without images hurt conversion.', + recommendSeo: 'Add SEO metadata to products missing it', + recommendSeoCount: 'Missing SEO titles/descriptions hurt search visibility.', + recommendCategory: 'Assign a category to uncategorized products', + recommendCategoryCount: 'Products without a category are harder to discover.', + recommendEmptyCategories: 'Categories without products', + recommendEmptyCategoriesCount: 'Consider merging or removing empty categories.', + recommendHomepage: 'Homepage is incomplete', + recommendStaticPages: 'Static pages are unpublished', + severity: { + info: 'Info', + warning: 'Warning', + critical: 'Critical', + }, }, adminShell: { skipToContent: 'Skip to content', diff --git a/src/app/i18n/hy.ts b/src/app/i18n/hy.ts index 5dde690..2b243cf 100644 --- a/src/app/i18n/hy.ts +++ b/src/app/i18n/hy.ts @@ -1589,10 +1589,100 @@ export const hy: Translations = { adminMonitoring: { eventsEmptyTitle: 'Իրադարձություններ չեն գտնվել', eventsEmptyDescription: 'Փորձեք այլ կատեգորիայի ֆիլտր կամ որոնման բառ։', + systemState: 'Համակարգի վիճակ', + backendConnectivity: 'Բեքենդ', + connected: 'Կապակցված է', + loading: 'Բեռնվում է', + draftState: 'Սևագիր', + unsavedChanges: 'Չպահպանված փոփոխություններ', + noDraft: 'Սևագիր չկա', + lastSaved: 'Վերջին պահպանում', + lastPublished: 'Վերջին հրապարակում', + syncStatus: 'Համաժամացում', }, adminAnalytics: { topProductsEmptyTitle: 'Այս ժամանակահատվածում ապրանքների վաճառք չկա', topProductsEmptyDescription: 'Փորձեք ընտրել ավելի լայն ամսաթվերի միջակայք։', + print: 'Տպել', + tabsLabel: 'Վերլուծության բաժիններ', + tab: { + overview: 'Ընդհանուր', + health: 'Առողջություն', + products: 'Ապրանքներ', + customers: 'Հաճախորդներ', + search: 'Որոնում', + traffic: 'Թրաֆիք', + recommendations: 'Առաջարկություններ', + }, + revenue: 'Եկամուտ', + orders: 'Պատվերներ', + customers: 'Հաճախորդներ', + avgOrderValue: 'Միջին պատվեր', + conversion: 'Փոխարկում', + completionPercent: 'Ավարտվածություն', + unknown: 'Անհայտ', + salesChart: 'Վաճառքներ', + quantitySold: 'Վաճառված քանակ', + lowStock: 'Ցածր պաշար', + lowStockEmptyTitle: 'Ցածր պաշարով ապրանքներ չկան', + quantity: 'Քանակ', + status: 'Կարգավիճակ', + recentActivity: 'Վերջին ակտիվություն', + recentActivityEmptyTitle: 'Վերջին ակտիվություն չկա', + activityDraftSaved: 'Սևագիրը պահպանվեց', + activityPublished: 'Հրապարակվեց', + warnings: 'Զգուշացումներ', + topViewed: 'Ամենադիտված', + hiddenProducts: 'Թաքցված ապրանքներ', + archivedProducts: 'Արխիվացված ապրանքներ', + mostReviewed: 'Ամենաշատ գնահատված', + worstRated: 'Ամենացածր վարկանիշ', + noReviewedProducts: 'Դեռ գնահատված ապրանքներ չկան', + reviewCount: 'Կարծիքներ', + avgRating: 'Միջին վարկանիշ', + newCustomers: 'Նոր հաճախորդներ', + returningCustomers: 'Կրկնվող հաճախորդներ', + averageSpend: 'Միջին ծախս', + retention: 'Պահպանում', + searchAnalytics: 'Որոնման վերլուծություն', + trafficAnalytics: 'Թրաֆիք', + availableAfterBackend: 'Հասանելի կլինի բեքենդի ինտեգրումից հետո։', + pendingBackend: 'Սպասում է բեքենդին', + pendingBackendHint: 'Որոնման հարցումների հետագծում դեռ չկա։', + funnelsHeatmaps: 'Ձագարներ և ջերմային քարտեզներ', + visitors: 'Այցելուներ', + products: 'Ապրանքներ', + categories: 'Կատեգորիաներ', + }, + adminMarketplaceHealth: { + title: 'Շուկայի առողջություն', + complete: 'ավարտված', + recommendationsTitle: 'Առաջարկություններ', + recommendNone: 'Ամեն ինչ կարգին է։', + checkImages: 'Ապրանքներն ունեն նկարներ', + checkSeo: 'Ապրանքներն ունեն SEO տվյալներ', + checkCategories: 'Կատեգորիաներն ունեն ապրանքներ', + checkReviews: 'Կարծիքները մոդերացվում են', + checkOrders: 'Պատվերները մշակվում են', + checkTranslations: 'Թարգմանությունները լրացված են', + checkHomepage: 'Գլխավոր էջը կարգավորված է', + checkBackend: 'Կապ բեքենդի հետ', + checkPerformance: 'Արտադրողականություն', + recommendImages: 'Ավելացրեք նկարներ դրանք չունեցող ապրանքներին', + recommendImagesCount: 'Նկարներ չունեցող ապրանքները նվազեցնում են փոխարկումը։', + recommendSeo: 'Լրացրեք SEO տվյալները դրանք չունեցող ապրանքների համար', + recommendSeoCount: 'SEO-ի բացակայությունը վատացնում է որոնման տեսանելիությունը։', + recommendCategory: 'Նշանակեք կատեգորիա առանց կատեգորիայի ապրանքներին', + recommendCategoryCount: 'Կատեգորիա չունեցող ապրանքներն ավելի դժվար են գտնվում։', + recommendEmptyCategories: 'Ապրանք չունեցող կատեգորիաներ', + recommendEmptyCategoriesCount: 'Դիտարկեք դատարկ կատեգորիաների միավորումը կամ հեռացումը։', + recommendHomepage: 'Գլխավոր էջը թերի է', + recommendStaticPages: 'Կան չհրապարակված ստատիկ էջեր', + severity: { + info: 'Տեղեկատվություն', + warning: 'Ուշադրություն', + critical: 'Կրիտիկական', + }, }, adminShell: { skipToContent: 'Անցնել բովանդակությանը', diff --git a/src/app/i18n/ru.ts b/src/app/i18n/ru.ts index 210fe63..1d67272 100644 --- a/src/app/i18n/ru.ts +++ b/src/app/i18n/ru.ts @@ -1589,10 +1589,100 @@ export const ru: Translations = { adminMonitoring: { eventsEmptyTitle: 'События не найдены', eventsEmptyDescription: 'Попробуйте изменить фильтр категории или поисковый запрос.', + systemState: 'Состояние системы', + backendConnectivity: 'Бэкенд', + connected: 'Подключено', + loading: 'Загрузка', + draftState: 'Черновик', + unsavedChanges: 'Есть несохранённые изменения', + noDraft: 'Черновика нет', + lastSaved: 'Последнее сохранение', + lastPublished: 'Последняя публикация', + syncStatus: 'Синхронизация', }, adminAnalytics: { topProductsEmptyTitle: 'Нет продаж товаров за этот период', topProductsEmptyDescription: 'Попробуйте выбрать более широкий диапазон дат.', + print: 'Печать', + tabsLabel: 'Разделы аналитики', + tab: { + overview: 'Обзор', + health: 'Здоровье', + products: 'Товары', + customers: 'Клиенты', + search: 'Поиск', + traffic: 'Трафик', + recommendations: 'Рекомендации', + }, + revenue: 'Выручка', + orders: 'Заказы', + customers: 'Клиенты', + avgOrderValue: 'Средний чек', + conversion: 'Конверсия', + completionPercent: 'Готовность', + unknown: 'Неизвестно', + salesChart: 'Продажи', + quantitySold: 'Продано, шт.', + lowStock: 'Мало на складе', + lowStockEmptyTitle: 'Нет товаров с низким остатком', + quantity: 'Количество', + status: 'Статус', + recentActivity: 'Недавняя активность', + recentActivityEmptyTitle: 'Нет недавней активности', + activityDraftSaved: 'Черновик сохранён', + activityPublished: 'Опубликовано', + warnings: 'Предупреждения', + topViewed: 'Самые просматриваемые', + hiddenProducts: 'Скрытые товары', + archivedProducts: 'Архивные товары', + mostReviewed: 'Больше всего отзывов', + worstRated: 'Худший рейтинг', + noReviewedProducts: 'Пока нет товаров с отзывами', + reviewCount: 'Отзывы', + avgRating: 'Средний рейтинг', + newCustomers: 'Новые клиенты', + returningCustomers: 'Повторные клиенты', + averageSpend: 'Средние траты', + retention: 'Удержание', + searchAnalytics: 'Аналитика поиска', + trafficAnalytics: 'Трафик', + availableAfterBackend: 'Будет доступно после интеграции с бэкендом.', + pendingBackend: 'Ожидает бэкенд', + pendingBackendHint: 'Отслеживание поисковых запросов пока не реализовано.', + funnelsHeatmaps: 'Воронки и тепловые карты', + visitors: 'Посетители', + products: 'Товары', + categories: 'Категории', + }, + adminMarketplaceHealth: { + title: 'Здоровье маркетплейса', + complete: 'готово', + recommendationsTitle: 'Рекомендации', + recommendNone: 'Всё в порядке.', + checkImages: 'У товаров есть изображения', + checkSeo: 'У товаров заполнено SEO', + checkCategories: 'В категориях есть товары', + checkReviews: 'Отзывы модерируются', + checkOrders: 'Заказы обрабатываются', + checkTranslations: 'Переводы заполнены', + checkHomepage: 'Главная страница настроена', + checkBackend: 'Связь с бэкендом', + checkPerformance: 'Производительность', + recommendImages: 'Добавьте изображения товарам без них', + recommendImagesCount: 'Товары без изображений снижают конверсию.', + recommendSeo: 'Заполните SEO для товаров без него', + recommendSeoCount: 'Отсутствие SEO ухудшает видимость в поиске.', + recommendCategory: 'Назначьте категорию товарам без категории', + recommendCategoryCount: 'Товары без категории труднее найти.', + recommendEmptyCategories: 'Категории без товаров', + recommendEmptyCategoriesCount: 'Рассмотрите объединение или удаление пустых категорий.', + recommendHomepage: 'Главная страница не заполнена', + recommendStaticPages: 'Есть неопубликованные статические страницы', + severity: { + info: 'Инфо', + warning: 'Внимание', + critical: 'Критично', + }, }, adminShell: { skipToContent: 'Перейти к содержимому', diff --git a/src/app/i18n/translations.ts b/src/app/i18n/translations.ts index 8f67fa1..581ff11 100644 --- a/src/app/i18n/translations.ts +++ b/src/app/i18n/translations.ts @@ -1601,10 +1601,100 @@ export interface Translations { adminMonitoring: { eventsEmptyTitle: string; eventsEmptyDescription: string; + systemState: string; + backendConnectivity: string; + connected: string; + loading: string; + draftState: string; + unsavedChanges: string; + noDraft: string; + lastSaved: string; + lastPublished: string; + syncStatus: string; }; adminAnalytics: { topProductsEmptyTitle: string; topProductsEmptyDescription: string; + print: string; + tabsLabel: string; + tab: { + overview: string; + health: string; + products: string; + customers: string; + search: string; + traffic: string; + recommendations: string; + }; + revenue: string; + orders: string; + customers: string; + avgOrderValue: string; + conversion: string; + completionPercent: string; + unknown: string; + salesChart: string; + quantitySold: string; + lowStock: string; + lowStockEmptyTitle: string; + quantity: string; + status: string; + recentActivity: string; + recentActivityEmptyTitle: string; + activityDraftSaved: string; + activityPublished: string; + warnings: string; + topViewed: string; + hiddenProducts: string; + archivedProducts: string; + mostReviewed: string; + worstRated: string; + noReviewedProducts: string; + reviewCount: string; + avgRating: string; + newCustomers: string; + returningCustomers: string; + averageSpend: string; + retention: string; + searchAnalytics: string; + trafficAnalytics: string; + availableAfterBackend: string; + pendingBackend: string; + pendingBackendHint: string; + funnelsHeatmaps: string; + visitors: string; + products: string; + categories: string; + }; + adminMarketplaceHealth: { + title: string; + complete: string; + recommendationsTitle: string; + recommendNone: string; + checkImages: string; + checkSeo: string; + checkCategories: string; + checkReviews: string; + checkOrders: string; + checkTranslations: string; + checkHomepage: string; + checkBackend: string; + checkPerformance: string; + recommendImages: string; + recommendImagesCount: string; + recommendSeo: string; + recommendSeoCount: string; + recommendCategory: string; + recommendCategoryCount: string; + recommendEmptyCategories: string; + recommendEmptyCategoriesCount: string; + recommendHomepage: string; + recommendStaticPages: string; + severity: { + info: string; + warning: string; + critical: string; + }; }; adminShell: { skipToContent: string;