import { Injectable, inject } from '@angular/core'; import { Observable, of, map, switchMap } from 'rxjs'; import { CategoryFacade } from '../../facades/platform/category.facade'; import { ProductFacade } from '../../facades/platform/product.facade'; import { SectionConfig, WidgetConfig } from '../../shared/models/config'; import { WidgetManifestEntry, WidgetDataSourceName } from '../contracts/widget-manifest.contract'; import { BannerWidgetData, CategoriesWidgetData, FooterWidgetData, HeroWidgetData, HtmlWidgetData, PartnersWidgetData, ProductCollectionWidgetData } from '../contracts/widget-data.contract'; import { WidgetManifestService } from '../registry/widget-manifest.service'; import { Category } from '../../core/categories/models/category-domain.model'; import { Product } from '../../core/products/models/product-domain.model'; @Injectable({ providedIn: 'root' }) export class DataSourceResolverService { private readonly widgetManifest = inject(WidgetManifestService); private readonly categoryFacade = inject(CategoryFacade); private readonly productFacade = inject(ProductFacade); resolve(widget: WidgetConfig, section: SectionConfig): Observable { return this.widgetManifest.getWidget(widget.type).pipe( switchMap((definition) => this.resolveByDefinition(definition, widget, section)) ); } private resolveByDefinition(definition: WidgetManifestEntry | undefined, widget: WidgetConfig, section: SectionConfig): Observable { const settings = this.resolveSettings(definition, widget); const source = this.resolveSource(widget, settings, definition); switch (widget.type) { case 'hero': return of(this.toHeroData(section, settings)); case 'categories': return this.resolveCategories(section, widget, settings, source); case 'product-collection': case 'product-carousel': return this.resolveProductCollection(section, widget, settings, source); case 'banner': return of(this.toBannerData(section, settings)); case 'html': return of(this.toHtmlData(section, settings)); case 'partners': return of(this.toPartnersData(section, settings)); case 'footer': case 'footer-navigation': return of(this.toFooterData(section, settings)); case 'recently-viewed': return of({ section, settings }); default: return of({ section, settings }); } } private resolveSettings(definition: WidgetManifestEntry | undefined, widget: WidgetConfig): Record { const metadataSettings: Record = {}; if (widget.title != null) { metadataSettings['title'] = widget.title; } if (widget.subtitle != null) { metadataSettings['subtitle'] = widget.subtitle; } if (widget.animation != null) { metadataSettings['animation'] = widget.animation; } if (widget.style != null) { metadataSettings['style'] = widget.style; } if (widget.permissions != null) { metadataSettings['permissions'] = widget.permissions; } return { ...(definition?.defaultSettings ?? {}), ...metadataSettings, ...(widget.props ?? {}) }; } private resolveSource(widget: WidgetConfig, settings: Record, definition?: WidgetManifestEntry): WidgetDataSourceName | undefined { const source = settings['source'] ?? widget.props?.['source'] ?? definition?.defaultSettings?.['source']; return typeof source === 'string' ? source as WidgetDataSourceName : undefined; } private toHeroData(section: SectionConfig, settings: Record): HeroWidgetData { const rawSlides = settings['slides']; const slides = Array.isArray(rawSlides) ? rawSlides .filter((slide): slide is Record => !!slide && typeof slide === 'object') .map(slide => ({ title: String(slide['title'] ?? ''), subtitle: slide['subtitle'] != null ? String(slide['subtitle']) : undefined, ctaLabel: slide['ctaLabel'] != null ? String(slide['ctaLabel']) : undefined, })) .filter(slide => slide.title) : undefined; return { section, settings, title: String(settings['title'] ?? ''), subtitle: settings['subtitle'] != null ? String(settings['subtitle']) : undefined, ctaLabel: settings['ctaLabel'] != null ? String(settings['ctaLabel']) : undefined, slides, autoplay: settings['autoplay'] === true, }; } private resolveCategories(section: SectionConfig, widget: WidgetConfig, settings: Record, source?: WidgetDataSourceName): Observable { const title = String(settings['title'] ?? ''); const emptyMessage = settings['emptyMessage'] != null ? String(settings['emptyMessage']) : undefined; switch (source) { case 'parent': { const parentId = this.toNumber(settings['parentId']); if (parentId == null) { return of({ section, settings, title, categories: [], emptyMessage }); } return this.categoryFacade.getChildren(parentId).pipe( map((categories) => ({ section, settings, title, categories, emptyMessage })) ); } case 'manual': { const categories = Array.isArray(settings['categories']) ? settings['categories'] as Category[] : []; return of({ section, settings, title, categories, emptyMessage }); } case 'future': return of({ section, settings, title, categories: [], emptyMessage }); case 'root': default: return this.categoryFacade.getRootCategories().pipe( map((categories) => ({ section, settings, title, categories, emptyMessage })) ); } } private resolveProductCollection(section: SectionConfig, widget: WidgetConfig, settings: Record, source?: WidgetDataSourceName): Observable { const title = String(settings['title'] ?? ''); const actionLabel = settings['actionLabel'] != null ? String(settings['actionLabel']) : undefined; const emptyMessage = settings['emptyMessage'] != null ? String(settings['emptyMessage']) : undefined; const count = this.toNumber(settings['count']) ?? 8; const categoryId = this.toNumber(settings['categoryId']); switch (source) { case 'latest': return this.productFacade.getLatestProducts({ count }).pipe( map((result) => this.toProductCollectionData(section, settings, title, result.items, actionLabel, emptyMessage)) ); case 'category': if (categoryId == null) { return of(this.toProductCollectionData(section, settings, title, [], actionLabel, emptyMessage)); } return this.productFacade.getProductsByCategory(categoryId, { count }).pipe( map((result) => this.toProductCollectionData(section, settings, title, result.items, actionLabel, emptyMessage)) ); case 'related': { const productId = this.toNumber(settings['productId']); if (productId == null) { return of(this.toProductCollectionData(section, settings, title, [], actionLabel, emptyMessage)); } return this.productFacade.getRelatedProducts({ productID: productId, categoryID: categoryId ?? undefined, count }).pipe( map((result) => this.toProductCollectionData(section, settings, title, result.items, actionLabel, emptyMessage)) ); } case 'manual': { const products = Array.isArray(settings['products']) ? settings['products'] as Product[] : []; return of(this.toProductCollectionData(section, settings, title, products, actionLabel, emptyMessage)); } case 'future': return of(this.toProductCollectionData(section, settings, title, [], actionLabel, emptyMessage)); case 'featured': default: return this.productFacade.getFeaturedProducts({ count }).pipe( map((result) => this.toProductCollectionData(section, settings, title, result.items, actionLabel, emptyMessage)) ); } } private toProductCollectionData(section: SectionConfig, settings: Record, title: string, products: Product[], actionLabel?: string, emptyMessage?: string): ProductCollectionWidgetData { return { section, settings, title, products, actionLabel, emptyMessage }; } private toBannerData(section: SectionConfig, settings: Record): BannerWidgetData { return { section, settings, title: settings['title'] != null ? String(settings['title']) : undefined, subtitle: settings['subtitle'] != null ? String(settings['subtitle']) : undefined, imageUrl: settings['imageUrl'] != null ? String(settings['imageUrl']) : undefined, ctaLabel: settings['ctaLabel'] != null ? String(settings['ctaLabel']) : undefined, ctaHref: settings['ctaHref'] != null ? String(settings['ctaHref']) : undefined }; } private toHtmlData(section: SectionConfig, settings: Record): HtmlWidgetData { return { section, settings, html: String(settings['html'] ?? '') }; } private toPartnersData(section: SectionConfig, settings: Record): PartnersWidgetData { const logos = Array.isArray(settings['logos']) ? settings['logos'] as PartnersWidgetData['logos'] : []; return { section, settings, title: settings['title'] != null ? String(settings['title']) : undefined, logos }; } private toFooterData(section: SectionConfig, settings: Record): FooterWidgetData { const links = Array.isArray(settings['links']) ? settings['links'] as FooterWidgetData['links'] : []; return { section, settings, links }; } private toNumber(value: unknown): number | null { if (typeof value === 'number' && Number.isFinite(value)) { return value; } if (typeof value === 'string' && value.trim().length > 0) { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : null; } return null; } }