feat: close stub-page gaps - profile login/logout, admin Reports/Settings, Help/Docs links
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Sprint A: storefront header profile control (login/logout only, no menu), wired to existing customer Telegram auth (AuthService). Sprint B: backoffice/reports page, reuses AdminAnalyticsFacade (Sales, Top Products, Marketplace Health cards + CSV export). Sprint C: backoffice/settings page, admin UI density preference (comfortable/compact), localStorage-persisted, applied to app-table across all admin list pages. Sprint D: admin bottom-nav Help -> mailto using existing supportEmail, Documentation -> external link via new TenantConfig.documentationUrl. AdminNavLink gains externalHref for non-routerLink nav entries. Docs: docs/GLOBAL-SPRINT-PLAN.md tracks the full sprint breakdown. docs/COMING-SOON-AUDIT.md removed, folded into docs/KNOWN-ISSUES.md. docs/BACKEND.md updated with the new documentationUrl bootstrap field. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -254,6 +254,24 @@ const coreRoutes: Routes = [
|
||||
breadcrumb: [{ labelKey: 'adminShell.nav.analytics' }]
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'reports',
|
||||
loadComponent: () => import('./features/admin/reports/pages/admin-reports-page.component').then(m => m.AdminReportsPageComponent),
|
||||
data: {
|
||||
titleKey: 'adminShell.pages.reports.title',
|
||||
descriptionKey: 'adminShell.pages.reports.description',
|
||||
breadcrumb: [{ labelKey: 'adminShell.nav.reports' }]
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
loadComponent: () => import('./features/admin/settings/pages/admin-settings-page.component').then(m => m.AdminSettingsPageComponent),
|
||||
data: {
|
||||
titleKey: 'adminShell.pages.settings.title',
|
||||
descriptionKey: 'adminShell.pages.settings.description',
|
||||
breadcrumb: [{ labelKey: 'adminShell.nav.settings' }]
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'partners/seller-management',
|
||||
loadComponent: () => import('./features/admin/seller-management/pages/admin-seller-management-page.component').then(m => m.AdminSellerManagementPageComponent),
|
||||
|
||||
@@ -80,6 +80,19 @@
|
||||
</a>
|
||||
}
|
||||
|
||||
<!-- Profile (login/logout only) -->
|
||||
@if (headerConfig().showProfile) {
|
||||
@if (isAuthenticated()) {
|
||||
<button type="button" class="platform-ux-btn" (click)="logout()" [attr.aria-label]="'header.logout' | translate">
|
||||
<app-icon name="logOut" [size]="16" class="platform-ux-icon" />
|
||||
</button>
|
||||
} @else {
|
||||
<button type="button" class="platform-ux-btn" (click)="login()" [attr.aria-label]="'header.login' | translate">
|
||||
<app-icon name="user" [size]="16" class="platform-ux-icon" />
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
<!-- Region Selector (desktop only) -->
|
||||
@if (headerConfig().showRegion) {
|
||||
<div class="platform-region-selector platform-lang-desktop">
|
||||
@@ -147,4 +160,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-telegram-login />
|
||||
|
||||
|
||||
|
||||
@@ -14,10 +14,12 @@ import { FeatureConfigService } from '../../core/config/feature-config.service';
|
||||
import { DEFAULT_HEADER_CONFIG, DEFAULT_USER_EXPERIENCE_CONFIG } from '../../shared/models/config';
|
||||
import { StaticPageResolverService } from '../../core/config/static-page-resolver.service';
|
||||
import { IconComponent } from '../../shared/ui/icon/icon.component';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
import { TelegramLoginComponent } from '../telegram-login/telegram-login.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-header',
|
||||
imports: [RouterLink, RouterLinkActive, LogoComponent, LanguageSelectorComponent, RegionSelectorComponent, LangRoutePipe, TranslatePipe, IconComponent],
|
||||
imports: [RouterLink, RouterLinkActive, LogoComponent, LanguageSelectorComponent, RegionSelectorComponent, LangRoutePipe, TranslatePipe, IconComponent, TelegramLoginComponent],
|
||||
templateUrl: './header.component.html',
|
||||
styleUrls: ['./header.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
@@ -35,7 +37,9 @@ export class HeaderComponent {
|
||||
private configService = inject(ConfigService);
|
||||
private featureConfig = inject(FeatureConfigService);
|
||||
private staticPageResolver = inject(StaticPageResolverService);
|
||||
private authService = inject(AuthService);
|
||||
|
||||
readonly isAuthenticated = this.authService.isAuthenticated;
|
||||
readonly wishlistCount = this.uxFacade.wishlistCount;
|
||||
readonly compareCount = this.uxFacade.compareCount;
|
||||
readonly userExperienceConfig = computed(() => this.resolveUserExperienceConfig());
|
||||
@@ -118,6 +122,14 @@ export class HeaderComponent {
|
||||
this.router.navigate([`/${lang}/compare`]);
|
||||
}
|
||||
|
||||
login(): void {
|
||||
this.authService.requestLogin();
|
||||
}
|
||||
|
||||
logout(): void {
|
||||
this.authService.logout();
|
||||
}
|
||||
|
||||
navigateToStatic(route: string): void {
|
||||
this.closeMenu();
|
||||
const lang = this.langService.currentLanguage();
|
||||
|
||||
@@ -31,7 +31,7 @@ const SHORTCUTS: AdminDashboardShortcut[] = [
|
||||
{ id: 'static-pages', icon: 'edit', labelKey: 'dashboard.actionStaticPages', route: ['backoffice', 'static-pages'] },
|
||||
{ id: 'orders', icon: 'cart', labelKey: 'dashboard.actionOrders', route: ['backoffice', 'orders'] },
|
||||
{ id: 'users', icon: 'users', labelKey: 'dashboard.actionUsers', route: ['backoffice', 'users'] },
|
||||
{ id: 'settings', icon: 'settings', labelKey: 'dashboard.shortcutSettings', route: [], comingSoon: true },
|
||||
{ id: 'settings', icon: 'settings', labelKey: 'dashboard.shortcutSettings', route: ['backoffice', 'settings'] },
|
||||
{ id: 'media-library', icon: 'images', labelKey: 'dashboard.actionMediaLibrary', route: ['backoffice', 'media'] },
|
||||
{ id: 'content', icon: 'alignLeft', labelKey: 'dashboard.shortcutContent', route: ['edit', 'static-pages'] },
|
||||
];
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<section class="admin-reports-page">
|
||||
@if (facade.loading()) {
|
||||
<div class="report-grid" role="status" aria-live="polite" aria-busy="true">
|
||||
@for (i of [1,2,3]; track i) {
|
||||
<app-skeleton shape="rect" height="140px" />
|
||||
}
|
||||
<span class="sr-only">{{ 'common.loading' | translate }}</span>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="report-grid">
|
||||
<div class="report-card">
|
||||
<h2>{{ 'adminReports.sales' | translate }}</h2>
|
||||
@if (facade.summary(); as summary) {
|
||||
<p class="report-summary">{{ summary.revenueTotal }} {{ summary.currency }} · {{ summary.ordersCount }} {{ 'adminAnalytics.orders' | translate }}</p>
|
||||
}
|
||||
<app-button variant="secondary" size="sm" (click)="exportSalesCsv()">{{ 'adminOrders.export' | translate }}</app-button>
|
||||
</div>
|
||||
|
||||
<div class="report-card">
|
||||
<h2>{{ 'adminReports.topProducts' | translate }}</h2>
|
||||
<p class="report-summary">{{ facade.topProducts().length }} {{ 'adminAnalytics.topProducts' | translate }}</p>
|
||||
<app-button variant="secondary" size="sm" (click)="exportTopProductsCsv()">{{ 'adminOrders.export' | translate }}</app-button>
|
||||
</div>
|
||||
|
||||
<div class="report-card">
|
||||
<h2>{{ 'adminReports.marketplaceHealth' | translate }}</h2>
|
||||
<p class="report-summary">{{ facade.healthCompletionPercent() }}% {{ 'adminMarketplaceHealth.complete' | translate }}</p>
|
||||
<app-button variant="secondary" size="sm" (click)="exportHealthCsv()">{{ 'adminOrders.export' | translate }}</app-button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
@@ -0,0 +1,19 @@
|
||||
.admin-reports-page { display: grid; gap: 16px; padding: 16px; max-width: 1100px; margin: 0 auto; }
|
||||
|
||||
.report-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; }
|
||||
|
||||
.report-card {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
align-content: start;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--border-color, #d3dad9);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-primary, #fff);
|
||||
}
|
||||
.report-card h2 { margin: 0; font-size: var(--font-size-xl, 1.125rem); }
|
||||
.report-summary { margin: 0; color: var(--text-secondary, #6b7280); font-size: var(--font-size-sm, 0.8125rem); }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.report-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
||||
import { AdminAnalyticsFacade } from '../../analytics/facade/admin-analytics.facade';
|
||||
import { TranslatePipe } from '../../../../i18n/translate.pipe';
|
||||
import { ButtonComponent } from '../../../../shared/ui/button/button.component';
|
||||
import { SkeletonComponent } from '../../../../shared/ui/skeleton/skeleton.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin-reports-page',
|
||||
standalone: true,
|
||||
imports: [TranslatePipe, ButtonComponent, SkeletonComponent],
|
||||
templateUrl: './admin-reports-page.component.html',
|
||||
styleUrls: ['./admin-reports-page.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class AdminReportsPageComponent {
|
||||
readonly facade = inject(AdminAnalyticsFacade);
|
||||
|
||||
constructor() {
|
||||
this.facade.load();
|
||||
}
|
||||
|
||||
exportSalesCsv(): void {
|
||||
this.download(this.facade.exportCsv(), 'sales-report.csv');
|
||||
}
|
||||
|
||||
exportTopProductsCsv(): void {
|
||||
this.download(this.facade.exportTopProductsCsv(), 'top-products-report.csv');
|
||||
}
|
||||
|
||||
exportHealthCsv(): void {
|
||||
this.download(this.facade.exportHealthCsv(), 'marketplace-health-report.csv');
|
||||
}
|
||||
|
||||
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 = filename;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<section class="admin-settings-page">
|
||||
<div class="settings-card">
|
||||
<h2>{{ 'adminSettings.density' | translate }}</h2>
|
||||
<p class="settings-explain">{{ 'adminSettings.densityExplain' | translate }}</p>
|
||||
<label class="toggle-row">
|
||||
<app-toggle
|
||||
[ngModel]="preferences.density() === 'compact'"
|
||||
(ngModelChange)="onCompactToggle($event)"
|
||||
[ariaLabel]="'adminSettings.densityCompact' | translate"
|
||||
/>
|
||||
<span>{{ 'adminSettings.densityCompact' | translate }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,14 @@
|
||||
.admin-settings-page { display: grid; gap: 16px; padding: 16px; max-width: 720px; margin: 0 auto; }
|
||||
|
||||
.settings-card {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--border-color, #d3dad9);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-primary, #fff);
|
||||
}
|
||||
.settings-card h2 { margin: 0; font-size: var(--font-size-xl, 1.125rem); }
|
||||
.settings-explain { margin: 0; color: var(--text-secondary, #6b7280); font-size: var(--font-size-sm, 0.8125rem); }
|
||||
|
||||
.toggle-row { display: flex; align-items: center; gap: 8px; font-weight: var(--font-weight-normal, 400); }
|
||||
@@ -0,0 +1,21 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { AdminPreferencesService } from '../services/admin-preferences.service';
|
||||
import { TranslatePipe } from '../../../../i18n/translate.pipe';
|
||||
import { ToggleComponent } from '../../../../shared/ui/toggle/toggle.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin-settings-page',
|
||||
standalone: true,
|
||||
imports: [FormsModule, TranslatePipe, ToggleComponent],
|
||||
templateUrl: './admin-settings-page.component.html',
|
||||
styleUrls: ['./admin-settings-page.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class AdminSettingsPageComponent {
|
||||
readonly preferences = inject(AdminPreferencesService);
|
||||
|
||||
onCompactToggle(compact: boolean): void {
|
||||
this.preferences.setDensity(compact ? 'compact' : 'comfortable');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Injectable, Signal, inject, signal } from '@angular/core';
|
||||
import { LocalStorageService } from '../../../../core/storage/local-storage.service';
|
||||
|
||||
export type AdminUiDensity = 'comfortable' | 'compact';
|
||||
|
||||
const DENSITY_KEY = 'adminPreferences.density.v1';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AdminPreferencesService {
|
||||
private readonly localStorage = inject(LocalStorageService);
|
||||
|
||||
private readonly densitySignal = signal<AdminUiDensity>(this.readStoredDensity());
|
||||
|
||||
readonly density: Signal<AdminUiDensity> = this.densitySignal.asReadonly();
|
||||
|
||||
setDensity(value: AdminUiDensity): void {
|
||||
this.densitySignal.set(value);
|
||||
this.localStorage.setItem(DENSITY_KEY, value);
|
||||
}
|
||||
|
||||
private readStoredDensity(): AdminUiDensity {
|
||||
return this.localStorage.getItem(DENSITY_KEY) === 'compact' ? 'compact' : 'comfortable';
|
||||
}
|
||||
}
|
||||
@@ -44,8 +44,20 @@
|
||||
</ul>
|
||||
|
||||
<ul class="admin-layout__nav-list admin-layout__nav-list--bottom">
|
||||
@for (entry of navBottom; track $index) {
|
||||
@if (entry.type === 'link' && entry.comingSoon) {
|
||||
@for (entry of navBottom(); track $index) {
|
||||
@if (entry.type === 'link' && entry.externalHref) {
|
||||
<li>
|
||||
<a
|
||||
class="admin-layout__nav-link"
|
||||
[href]="entry.externalHref"
|
||||
[attr.target]="entry.externalHref.startsWith('mailto:') ? null : '_blank'"
|
||||
[attr.rel]="entry.externalHref.startsWith('mailto:') ? null : 'noopener'"
|
||||
>
|
||||
<app-icon [name]="entry.icon" [size]="18" />
|
||||
<span class="admin-layout__nav-label">{{ entry.labelKey | translate }}</span>
|
||||
</a>
|
||||
</li>
|
||||
} @else if (entry.type === 'link' && entry.comingSoon) {
|
||||
<li>
|
||||
<button type="button" class="admin-layout__nav-link admin-layout__nav-link--disabled" disabled>
|
||||
<app-icon [name]="entry.icon" [size]="18" />
|
||||
@@ -151,7 +163,7 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main id="admin-content" class="admin-layout__content" tabindex="-1">
|
||||
<main id="admin-content" class="admin-layout__content" [class.admin-density-compact]="density() === 'compact'" tabindex="-1">
|
||||
<router-outlet></router-outlet>
|
||||
</main>
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@ import { LanguageService } from '../../../services/language.service';
|
||||
import { AdminAuthService } from '../../../core/admin-auth/admin-auth.service';
|
||||
import { ADMIN_NAV_BOTTOM, ADMIN_NAV_PRIMARY, AdminBreadcrumbEntry, AdminNavEntry } from './admin-nav.model';
|
||||
import { IconComponent } from '../../../shared/ui/icon/icon.component';
|
||||
import { AdminPreferencesService } from '../settings/services/admin-preferences.service';
|
||||
import { UiRuntimeFacade } from '../../../facades/runtime/ui-runtime.facade';
|
||||
import { ConfigService } from '../../../core/config/config.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin-layout',
|
||||
@@ -25,12 +28,35 @@ export class AdminLayoutComponent {
|
||||
private readonly translate = inject(TranslateService);
|
||||
private readonly languageService = inject(LanguageService);
|
||||
private readonly adminAuth = inject(AdminAuthService);
|
||||
private readonly preferences = inject(AdminPreferencesService);
|
||||
private readonly uiRuntime = inject(UiRuntimeFacade);
|
||||
private readonly configService = inject(ConfigService);
|
||||
|
||||
readonly density = this.preferences.density;
|
||||
|
||||
private readonly drawerEl = viewChild<ElementRef<HTMLElement>>('drawer');
|
||||
private readonly menuToggleEl = viewChild<ElementRef<HTMLElement>>('menuToggle');
|
||||
|
||||
readonly navPrimary: AdminNavEntry[] = ADMIN_NAV_PRIMARY;
|
||||
readonly navBottom: AdminNavEntry[] = ADMIN_NAV_BOTTOM;
|
||||
|
||||
readonly navBottom = computed<AdminNavEntry[]>(() => {
|
||||
this.configService.bootstrapRevision();
|
||||
const supportEmail = this.uiRuntime.contactEmail();
|
||||
const documentationUrl = this.configService.getBootstrapSnapshot()?.tenant?.documentationUrl;
|
||||
|
||||
return ADMIN_NAV_BOTTOM.map(entry => {
|
||||
if (entry.type !== 'link') {
|
||||
return entry;
|
||||
}
|
||||
if (entry.id === 'help' && supportEmail) {
|
||||
return { ...entry, comingSoon: false, externalHref: `mailto:${supportEmail}` };
|
||||
}
|
||||
if (entry.id === 'documentation' && documentationUrl) {
|
||||
return { ...entry, comingSoon: false, externalHref: documentationUrl };
|
||||
}
|
||||
return entry;
|
||||
});
|
||||
});
|
||||
|
||||
readonly mobileDrawerOpen = signal(false);
|
||||
readonly notificationsOpen = signal(false);
|
||||
|
||||
@@ -11,6 +11,8 @@ export interface AdminNavLink {
|
||||
absolutePath?: string[];
|
||||
/** No route exists yet - rendered as a disabled item with a "coming soon" badge. */
|
||||
comingSoon?: boolean;
|
||||
/** External URL (mailto: or https:). Mutually exclusive with path/absolutePath/comingSoon. */
|
||||
externalHref?: string;
|
||||
}
|
||||
|
||||
export interface AdminNavGroup {
|
||||
@@ -36,18 +38,24 @@ export const ADMIN_NAV_PRIMARY: AdminNavEntry[] = [
|
||||
{ type: 'link', id: 'customers', icon: 'user', labelKey: 'adminShell.nav.customers', path: ['customers'] },
|
||||
{ type: 'link', id: 'transactions', icon: 'creditCard', labelKey: 'adminShell.nav.transactions', path: ['transactions'] },
|
||||
{ type: 'link', id: 'moderation', icon: 'star', labelKey: 'adminShell.nav.moderation', path: ['moderation'] },
|
||||
{ type: 'link', id: 'reports', icon: 'chartBar', labelKey: 'adminShell.nav.reports', comingSoon: true },
|
||||
{ type: 'link', id: 'reports', icon: 'chartBar', labelKey: 'adminShell.nav.reports', path: ['reports'] },
|
||||
{ type: 'group', labelKey: 'adminShell.nav.partnersGroup' },
|
||||
{ type: 'link', id: 'seller-management', icon: 'store', labelKey: 'adminShell.nav.sellerManagement', path: ['partners', 'seller-management'] },
|
||||
{ type: 'link', id: 'content', icon: 'edit', labelKey: 'adminShell.nav.content', absolutePath: ['edit', 'static-pages'] },
|
||||
{ type: 'link', id: 'media', icon: 'images', labelKey: 'adminShell.nav.mediaLibrary', path: ['media'] },
|
||||
{ type: 'link', id: 'marketplace-builder', icon: 'network', labelKey: 'adminShell.nav.marketplaceBuilder', absolutePath: ['edit'] },
|
||||
{ type: 'link', id: 'users', icon: 'users', labelKey: 'adminShell.nav.users', path: ['users'] },
|
||||
{ type: 'link', id: 'settings', icon: 'settings', labelKey: 'adminShell.nav.settings', comingSoon: true },
|
||||
{ type: 'link', id: 'settings', icon: 'settings', labelKey: 'adminShell.nav.settings', path: ['settings'] },
|
||||
{ type: 'link', id: 'monitoring', icon: 'monitor', labelKey: 'adminShell.nav.monitoring', path: ['monitoring'] },
|
||||
{ type: 'link', id: 'analytics', icon: 'chartLine', labelKey: 'adminShell.nav.analytics', path: ['analytics'] },
|
||||
];
|
||||
|
||||
/**
|
||||
* `documentation`/`help` externalHref is resolved at runtime from bootstrap
|
||||
* config (tenant.documentationUrl / branding.supportEmail) — see
|
||||
* `AdminLayoutComponent.navBottom`. Static comingSoon here is the fallback
|
||||
* when that data is absent.
|
||||
*/
|
||||
export const ADMIN_NAV_BOTTOM: AdminNavEntry[] = [
|
||||
{ type: 'link', id: 'documentation', icon: 'book', labelKey: 'adminShell.nav.documentation', comingSoon: true },
|
||||
{ type: 'link', id: 'help', icon: 'help', labelKey: 'adminShell.nav.help', comingSoon: true },
|
||||
|
||||
@@ -12,6 +12,8 @@ export const en: Translations = {
|
||||
compare: 'Compare',
|
||||
openMenu: 'Open menu',
|
||||
closeMenu: 'Close menu',
|
||||
login: 'Log in',
|
||||
logout: 'Log out',
|
||||
},
|
||||
footer: {
|
||||
description: 'A modern marketplace for comfortable shopping',
|
||||
@@ -1909,6 +1911,16 @@ export const en: Translations = {
|
||||
lastPublished: 'Last published',
|
||||
syncStatus: 'Sync',
|
||||
},
|
||||
adminReports: {
|
||||
sales: 'Sales',
|
||||
topProducts: 'Top Products',
|
||||
marketplaceHealth: 'Marketplace Health',
|
||||
},
|
||||
adminSettings: {
|
||||
density: 'Table density',
|
||||
densityExplain: 'Reduce row padding across backoffice list pages for a more compact view.',
|
||||
densityCompact: 'Compact rows',
|
||||
},
|
||||
adminAnalytics: {
|
||||
topProductsEmptyTitle: 'No product sales in this period',
|
||||
topProductsEmptyDescription: 'Try a wider date range.',
|
||||
@@ -2060,6 +2072,8 @@ export const en: Translations = {
|
||||
monitoring: { title: 'Monitoring', description: 'Queue, webhook, and system event status' },
|
||||
analytics: { title: 'Analytics', description: 'Sales, products, and shopper behavior' },
|
||||
sellerManagement: { title: 'Seller Management', description: 'Onboard and manage independent sellers on your marketplace' },
|
||||
reports: { title: 'Reports', description: 'Export sales, product, and marketplace health reports' },
|
||||
settings: { title: 'Settings', description: 'Backoffice display preferences' },
|
||||
},
|
||||
},
|
||||
adminSellerManagement: {
|
||||
|
||||
@@ -12,6 +12,8 @@ export const hy: Translations = {
|
||||
compare: 'Համեմատում',
|
||||
openMenu: 'Բացել մենյուն',
|
||||
closeMenu: 'Փակել մենյուն',
|
||||
login: 'Մուտք',
|
||||
logout: 'Ելք',
|
||||
},
|
||||
footer: {
|
||||
description: 'Ժամանակակից մարքեթփլեյս հարմար գնումների համար',
|
||||
@@ -1904,6 +1906,16 @@ export const hy: Translations = {
|
||||
lastPublished: 'Վերջին հրապարակում',
|
||||
syncStatus: 'Համաժամացում',
|
||||
},
|
||||
adminReports: {
|
||||
sales: 'Վաճառքներ',
|
||||
topProducts: 'Լավագույն ապրանքներ',
|
||||
marketplaceHealth: 'Մարքեթփլեյսի վիճակ',
|
||||
},
|
||||
adminSettings: {
|
||||
density: 'Աղյուսակի խտություն',
|
||||
densityExplain: 'Փոքրացնել տողերի հեռավորությունը ադմինիստրատիվ վահանակի ցուցակներում՝ ավելի կոմպակտ տեսքի համար։',
|
||||
densityCompact: 'Կոմպակտ տողեր',
|
||||
},
|
||||
adminAnalytics: {
|
||||
topProductsEmptyTitle: 'Այս ժամանակահատվածում ապրանքների վաճառք չկա',
|
||||
topProductsEmptyDescription: 'Փորձեք ընտրել ավելի լայն ամսաթվերի միջակայք։',
|
||||
@@ -2055,6 +2067,8 @@ export const hy: Translations = {
|
||||
monitoring: { title: 'Մոնիտորինգ', description: 'Հերթերի, webhook-ների և համակարգային իրադարձությունների վիճակը' },
|
||||
analytics: { title: 'Վերլուծություն', description: 'Վաճառքներ, ապրանքներ և գնորդների վարքագիծ' },
|
||||
sellerManagement: { title: 'Վաճառողների կառավարում', description: 'Թույլ տվեք անկախ վաճառողներին միանալ ձեր մարքեթփլեյսին՝ պահպանելով կենտրոնացված կառավարումը' },
|
||||
reports: { title: 'Հաշվետվություններ', description: 'Արտահանել վաճառքների, ապրանքների և մարքեթփլեյսի վիճակի հաշվետվություններ' },
|
||||
settings: { title: 'Կարգավորումներ', description: 'Ադմինիստրատիվ վահանակի ցուցադրման նախապատվություններ' },
|
||||
},
|
||||
},
|
||||
adminSellerManagement: {
|
||||
|
||||
@@ -12,6 +12,8 @@ export const ru: Translations = {
|
||||
compare: 'Сравнение',
|
||||
openMenu: 'Открыть меню',
|
||||
closeMenu: 'Закрыть меню',
|
||||
login: 'Войти',
|
||||
logout: 'Выйти',
|
||||
},
|
||||
footer: {
|
||||
description: 'Современный маркетплейс для комфортных покупок',
|
||||
@@ -1904,6 +1906,16 @@ export const ru: Translations = {
|
||||
lastPublished: 'Последняя публикация',
|
||||
syncStatus: 'Синхронизация',
|
||||
},
|
||||
adminReports: {
|
||||
sales: 'Продажи',
|
||||
topProducts: 'Топ товаров',
|
||||
marketplaceHealth: 'Состояние маркетплейса',
|
||||
},
|
||||
adminSettings: {
|
||||
density: 'Плотность таблиц',
|
||||
densityExplain: 'Уменьшить отступы строк в списках панели управления для более компактного вида.',
|
||||
densityCompact: 'Компактные строки',
|
||||
},
|
||||
adminAnalytics: {
|
||||
topProductsEmptyTitle: 'Нет продаж товаров за этот период',
|
||||
topProductsEmptyDescription: 'Попробуйте выбрать более широкий диапазон дат.',
|
||||
@@ -2055,6 +2067,8 @@ export const ru: Translations = {
|
||||
monitoring: { title: 'Мониторинг', description: 'Состояние очередей, вебхуков и системных событий' },
|
||||
analytics: { title: 'Аналитика', description: 'Продажи, товары и поведение покупателей' },
|
||||
sellerManagement: { title: 'Управление продавцами', description: 'Подключайте независимых продавцов к вашему маркетплейсу с централизованным администрированием' },
|
||||
reports: { title: 'Отчёты', description: 'Экспорт отчётов по продажам, товарам и состоянию маркетплейса' },
|
||||
settings: { title: 'Настройки', description: 'Настройки отображения панели управления' },
|
||||
},
|
||||
},
|
||||
adminSellerManagement: {
|
||||
|
||||
@@ -10,6 +10,8 @@ export interface Translations {
|
||||
compare: string;
|
||||
openMenu: string;
|
||||
closeMenu: string;
|
||||
login: string;
|
||||
logout: string;
|
||||
};
|
||||
footer: {
|
||||
description: string;
|
||||
@@ -1917,6 +1919,16 @@ export interface Translations {
|
||||
lastPublished: string;
|
||||
syncStatus: string;
|
||||
};
|
||||
adminReports: {
|
||||
sales: string;
|
||||
topProducts: string;
|
||||
marketplaceHealth: string;
|
||||
};
|
||||
adminSettings: {
|
||||
density: string;
|
||||
densityExplain: string;
|
||||
densityCompact: string;
|
||||
};
|
||||
adminAnalytics: {
|
||||
topProductsEmptyTitle: string;
|
||||
topProductsEmptyDescription: string;
|
||||
@@ -2068,6 +2080,8 @@ export interface Translations {
|
||||
monitoring: { title: string; description: string };
|
||||
analytics: { title: string; description: string };
|
||||
sellerManagement: { title: string; description: string };
|
||||
reports: { title: string; description: string };
|
||||
settings: { title: string; description: string };
|
||||
};
|
||||
};
|
||||
adminSellerManagement: {
|
||||
|
||||
@@ -14,4 +14,5 @@ export interface TenantConfig {
|
||||
defaultCurrency: string;
|
||||
supportedCurrencies: string[];
|
||||
timezone: string;
|
||||
documentationUrl?: string;
|
||||
}
|
||||
|
||||
@@ -50,3 +50,9 @@ app-table {
|
||||
background: var(--bg-secondary, #f4f4f5);
|
||||
}
|
||||
}
|
||||
|
||||
.admin-density-compact .app-table {
|
||||
th, td {
|
||||
padding: var(--space-xs, 0.25rem) var(--space-md, 1rem);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"supportedLocales": ["ru", "en", "hy"],
|
||||
"defaultCurrency": "RUB",
|
||||
"supportedCurrencies": ["RUB", "USD", "EUR", "AMD"],
|
||||
"timezone": "Europe/Moscow"
|
||||
"timezone": "Europe/Moscow",
|
||||
"documentationUrl": "https://docs.marketplace.local"
|
||||
},
|
||||
"branding": {
|
||||
"brandName": "Marketplace",
|
||||
|
||||
Reference in New Issue
Block a user