diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index eda6bfb..8a4b465 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -218,6 +218,15 @@ const coreRoutes: Routes = [ breadcrumb: [{ labelKey: 'adminShell.nav.finance' }] } }, + { + path: 'marketplaces', + loadComponent: () => import('./features/admin/marketplaces/pages/admin-marketplaces-page.component').then(m => m.AdminMarketplacesPageComponent), + data: { + titleKey: 'adminShell.nav.marketplaces', + descriptionKey: 'adminShell.nav.marketplaces', + breadcrumb: [{ labelKey: 'adminShell.nav.marketplaces' }] + } + }, { path: 'moderation', loadComponent: () => import('./features/admin/moderation/pages/admin-reviews-list-page.component').then(m => m.AdminReviewsListPageComponent), diff --git a/src/app/core/marketplace-registry/models/marketplace.model.ts b/src/app/core/marketplace-registry/models/marketplace.model.ts new file mode 100644 index 0000000..585d70e --- /dev/null +++ b/src/app/core/marketplace-registry/models/marketplace.model.ts @@ -0,0 +1,28 @@ +/** Per docs/backend/PHASE-9-TENANT-REGISTRY-DOMAINS-CONTRACT.md §1-2. */ +export type MarketplaceLifecycleState = + | 'draft' | 'configured' | 'content_ready' | 'domains_planned' + | 'staging_live' | 'qa_passed' | 'production_ready' | 'live' | 'paused' | 'archived'; + +export interface Marketplace { + id: string; + name: string; + code: string; + type: 'commerce' | 'mall_directory' | 'hybrid' | 'single_brand'; + ownerId: string; + countries: string[]; + currencies: string[]; + lifecycleState: MarketplaceLifecycleState; +} + +export interface MarketplaceDomain { + marketplaceId: string; + domain: string; + type: 'production' | 'www' | 'staging' | 'preview' | 'api' | 'seller'; + status: 'planned' | 'dns_pending' | 'ssl_pending' | 'active' | 'failed'; +} + +export interface LifecycleAdvanceResult { + currentState: MarketplaceLifecycleState; + nextState: MarketplaceLifecycleState | null; + blockers: string[]; +} diff --git a/src/app/core/marketplace-registry/services/marketplace-gateway.interface.ts b/src/app/core/marketplace-registry/services/marketplace-gateway.interface.ts new file mode 100644 index 0000000..97e1730 --- /dev/null +++ b/src/app/core/marketplace-registry/services/marketplace-gateway.interface.ts @@ -0,0 +1,9 @@ +import { Observable } from 'rxjs'; +import { LifecycleAdvanceResult, Marketplace, MarketplaceDomain } from '../models/marketplace.model'; + +/** Per docs/backend/PHASE-9-TENANT-REGISTRY-DOMAINS-CONTRACT.md §2-4. */ +export interface MarketplaceGateway { + loadMarketplaces(): Observable; + loadDomains(marketplaceId: string): Observable; + loadLifecycle(marketplaceId: string): Observable; +} diff --git a/src/app/core/marketplace-registry/services/marketplace-gateway.token.ts b/src/app/core/marketplace-registry/services/marketplace-gateway.token.ts new file mode 100644 index 0000000..216c999 --- /dev/null +++ b/src/app/core/marketplace-registry/services/marketplace-gateway.token.ts @@ -0,0 +1,9 @@ +import { InjectionToken, inject } from '@angular/core'; +import { MarketplaceGateway } from './marketplace-gateway.interface'; +import { MarketplaceLocalGateway } from './marketplace-local.gateway'; + +/** Swap point for docs/backend/PHASE-9-TENANT-REGISTRY-DOMAINS-CONTRACT.md. */ +export const MARKETPLACE_GATEWAY = new InjectionToken('MARKETPLACE_GATEWAY', { + providedIn: 'root', + factory: () => inject(MarketplaceLocalGateway), +}); diff --git a/src/app/core/marketplace-registry/services/marketplace-local.gateway.ts b/src/app/core/marketplace-registry/services/marketplace-local.gateway.ts new file mode 100644 index 0000000..7e1fab2 --- /dev/null +++ b/src/app/core/marketplace-registry/services/marketplace-local.gateway.ts @@ -0,0 +1,42 @@ +import { Injectable, inject } from '@angular/core'; +import { Observable, map, of } from 'rxjs'; +import { LifecycleAdvanceResult, Marketplace, MarketplaceDomain } from '../models/marketplace.model'; +import { MarketplaceGateway } from './marketplace-gateway.interface'; +import { ConfigService } from '../../config/config.service'; + +/** + * The platform runs one live tenant per deployment today - there is no + * registry of multiple marketplaces anywhere in the codebase. This derives + * a single-row "registry" from the current tenant's own bootstrap so the + * shape is real, ready to become a genuine multi-row registry once + * docs/backend/PHASE-9-TENANT-REGISTRY-DOMAINS-CONTRACT.md ships. + */ +@Injectable({ providedIn: 'root' }) +export class MarketplaceLocalGateway implements MarketplaceGateway { + private readonly configService = inject(ConfigService); + + loadMarketplaces(): Observable { + const bootstrap = this.configService.getBootstrapSnapshot(); + return of([{ + id: 'current', + name: bootstrap?.branding?.brandName || 'This marketplace', + code: 'current', + type: 'commerce', + ownerId: 'unknown', + countries: [], + currencies: [], + lifecycleState: 'live', + }]); + } + + loadDomains(marketplaceId: string): Observable { + if (typeof window === 'undefined') { + return of([]); + } + return of([{ marketplaceId, domain: window.location.hostname, type: 'production', status: 'active' }]); + } + + loadLifecycle(_marketplaceId: string): Observable { + return of({ currentState: 'live', nextState: null, blockers: [] }); + } +} diff --git a/src/app/features/admin/marketplaces/facade/admin-marketplaces.facade.ts b/src/app/features/admin/marketplaces/facade/admin-marketplaces.facade.ts new file mode 100644 index 0000000..76cd30e --- /dev/null +++ b/src/app/features/admin/marketplaces/facade/admin-marketplaces.facade.ts @@ -0,0 +1,27 @@ +import { Injectable, inject, signal } from '@angular/core'; +import { take } from 'rxjs/operators'; +import { LifecycleAdvanceResult, Marketplace, MarketplaceDomain } from '../../../../core/marketplace-registry/models/marketplace.model'; +import { MARKETPLACE_GATEWAY } from '../../../../core/marketplace-registry/services/marketplace-gateway.token'; + +@Injectable({ providedIn: 'root' }) +export class AdminMarketplacesFacade { + private readonly gateway = inject(MARKETPLACE_GATEWAY); + + readonly marketplaces = signal([]); + readonly domains = signal([]); + readonly lifecycle = signal(null); + readonly loading = signal(false); + + load(): void { + this.loading.set(true); + this.gateway.loadMarketplaces().pipe(take(1)).subscribe(items => { + this.marketplaces.set(items); + this.loading.set(false); + const first = items[0]; + if (first) { + this.gateway.loadDomains(first.id).pipe(take(1)).subscribe(d => this.domains.set(d)); + this.gateway.loadLifecycle(first.id).pipe(take(1)).subscribe(l => this.lifecycle.set(l)); + } + }); + } +} diff --git a/src/app/features/admin/marketplaces/pages/admin-marketplaces-page.component.html b/src/app/features/admin/marketplaces/pages/admin-marketplaces-page.component.html new file mode 100644 index 0000000..61f8809 --- /dev/null +++ b/src/app/features/admin/marketplaces/pages/admin-marketplaces-page.component.html @@ -0,0 +1,25 @@ +
+
+

Marketplaces

+

Registry, domains, and release/lifecycle status. Single-tenant derived until docs/backend/PHASE-9-TENANT-REGISTRY-DOMAINS-CONTRACT.md ships a real registry.

+
+ + @for (m of facade.marketplaces(); track m.id) { +
+

{{ m.name }} {{ m.lifecycleState }}

+

Type: {{ m.type }} - Code: {{ m.code }}

+
+ } + +
+

Domains & Releases

+
    + @for (d of facade.domains(); track d.domain) { +
  • {{ d.domain }} - {{ d.status }} ({{ d.type }})
  • + } +
+ @if (facade.lifecycle(); as lifecycle) { +

Lifecycle: {{ lifecycle.currentState }} - blockers: {{ lifecycle.blockers.length === 0 ? 'none' : lifecycle.blockers.join(', ') }}

+ } +
+
diff --git a/src/app/features/admin/marketplaces/pages/admin-marketplaces-page.component.scss b/src/app/features/admin/marketplaces/pages/admin-marketplaces-page.component.scss new file mode 100644 index 0000000..2bf3571 --- /dev/null +++ b/src/app/features/admin/marketplaces/pages/admin-marketplaces-page.component.scss @@ -0,0 +1,20 @@ +.marketplaces-page { + display: flex; + flex-direction: column; + gap: 24px; + + &__card { + padding: 16px; + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + } + + ul { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 6px; + } +} diff --git a/src/app/features/admin/marketplaces/pages/admin-marketplaces-page.component.ts b/src/app/features/admin/marketplaces/pages/admin-marketplaces-page.component.ts new file mode 100644 index 0000000..9772c84 --- /dev/null +++ b/src/app/features/admin/marketplaces/pages/admin-marketplaces-page.component.ts @@ -0,0 +1,20 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { AdminMarketplacesFacade } from '../facade/admin-marketplaces.facade'; +import { BadgeComponent } from '../../../../shared/ui/badge/badge.component'; + +@Component({ + selector: 'app-admin-marketplaces-page', + standalone: true, + imports: [CommonModule, BadgeComponent], + templateUrl: './admin-marketplaces-page.component.html', + styleUrls: ['./admin-marketplaces-page.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class AdminMarketplacesPageComponent { + readonly facade = inject(AdminMarketplacesFacade); + + constructor() { + this.facade.load(); + } +} diff --git a/src/app/features/admin/shell/admin-nav.model.ts b/src/app/features/admin/shell/admin-nav.model.ts index d0aa9a5..d0ca1fa 100644 --- a/src/app/features/admin/shell/admin-nav.model.ts +++ b/src/app/features/admin/shell/admin-nav.model.ts @@ -31,6 +31,7 @@ export type AdminNavEntry = AdminNavLink | AdminNavGroup | AdminNavAction; export const ADMIN_NAV_PRIMARY: AdminNavEntry[] = [ { type: 'link', id: 'dashboard', icon: 'home', labelKey: 'adminShell.nav.dashboard', path: ['dashboard'] }, + { type: 'link', id: 'marketplaces', icon: 'network', labelKey: 'adminShell.nav.marketplaces', path: ['marketplaces'] }, { type: 'group', labelKey: 'adminShell.nav.catalogGroup' }, { type: 'link', id: 'products', icon: 'package', labelKey: 'adminShell.nav.products', path: ['products'] }, { type: 'link', id: 'categories', icon: 'tags', labelKey: 'adminShell.nav.categories', path: ['categories'] }, diff --git a/src/app/i18n/en.ts b/src/app/i18n/en.ts index b08f38e..018d1e0 100644 --- a/src/app/i18n/en.ts +++ b/src/app/i18n/en.ts @@ -2062,6 +2062,7 @@ export const en: Translations = { notifications: 'Notifications', integrations: 'Integrations', finance: 'Payments & Finance', + marketplaces: 'Marketplaces', reviews: 'Reviews', reports: 'Reports', partnersGroup: 'Partners', diff --git a/src/app/i18n/hy.ts b/src/app/i18n/hy.ts index 39e987a..8979b48 100644 --- a/src/app/i18n/hy.ts +++ b/src/app/i18n/hy.ts @@ -2056,6 +2056,7 @@ export const hy: Translations = { notifications: 'Ծանուցումներ', integrations: 'Ինտեգրումներ', finance: 'Վճարումներ և ֆինանսներ', + marketplaces: 'Մարկետփլեյսներ', transactions: 'Գործարքներ', reviews: 'Կարծիքներ', reports: 'Հաշվետվություններ', diff --git a/src/app/i18n/ru.ts b/src/app/i18n/ru.ts index b0e3afe..9f4126e 100644 --- a/src/app/i18n/ru.ts +++ b/src/app/i18n/ru.ts @@ -2056,6 +2056,7 @@ export const ru: Translations = { notifications: 'Уведомления', integrations: 'Интеграции', finance: 'Платежи и финансы', + marketplaces: 'Маркетплейсы', transactions: 'Транзакции', reviews: 'Отзывы', reports: 'Отчёты', diff --git a/src/app/i18n/translations.ts b/src/app/i18n/translations.ts index fb684fa..15fcbae 100644 --- a/src/app/i18n/translations.ts +++ b/src/app/i18n/translations.ts @@ -2071,6 +2071,7 @@ export interface Translations { notifications: string; integrations: string; finance: string; + marketplaces: string; reports: string; partnersGroup: string; sellerManagement: string;