diff --git a/src/app/core/services/tenant.interface.ts b/src/app/core/services/tenant.interface.ts index 51a87cf..0207fe4 100644 --- a/src/app/core/services/tenant.interface.ts +++ b/src/app/core/services/tenant.interface.ts @@ -9,8 +9,13 @@ export interface BankAccount { cvu: string; } +export interface WebsiteType { + codigo: string; + nombre: string; +} + export interface HeroConfig { - background_image?: string | null; + background_image_id?: string | null; title_html?: string | null; description_html?: string | null; button_text?: string | null; @@ -23,6 +28,13 @@ export interface EventConfig { dates?: string[] | null; } +export interface WebsiteExtras { + carousel?: string[]; + heroConfig?: HeroConfig | null; + eventConfig?: EventConfig | null; + [extraName: string]: unknown; +} + export interface Menu { id: number; code: string; @@ -60,14 +72,14 @@ export interface Tenant { footer_bg_color: string; header_logo: string; footer_logo: string; + website_type_code?: string | null; + website_type?: WebsiteType | null; + extras?: WebsiteExtras; selected_bank_account_id?: number | null; selected_bank_account?: BankAccount | null; - hero_config?: HeroConfig | null; - event_config?: EventConfig | null; search_product_layout?: 'row' | 'column_with_image' | 'column_with_cart'; search_group_layout?: 'paginated' | 'simple' | 'simple_vertical' | 'carousel'; search_items_per_page?: number; - main_carousel_images?: string[]; social_media?: SocialMedia[]; menues?: Menu[]; categories: Category[]; diff --git a/src/app/features/store/pages/store-home-page/store-home-page.component.html b/src/app/features/store/pages/store-home-page/store-home-page.component.html index d62e95a..514396a 100644 --- a/src/app/features/store/pages/store-home-page/store-home-page.component.html +++ b/src/app/features/store/pages/store-home-page/store-home-page.component.html @@ -1,19 +1,16 @@ -@if (tenant()?.main_carousel_images; as mainCarouselImages) { - @if (mainCarouselImages.length) { +@if (mainCarouselImages(); as images) { + @if (images.length) { } } -@if (tenant()?.hero_config || tenant()?.event_config) { - +@if (heroConfig() || eventConfig()) { + } @if (error()) { diff --git a/src/app/features/store/pages/store-home-page/store-home-page.component.spec.ts b/src/app/features/store/pages/store-home-page/store-home-page.component.spec.ts index 982483f..3c1e1c8 100644 --- a/src/app/features/store/pages/store-home-page/store-home-page.component.spec.ts +++ b/src/app/features/store/pages/store-home-page/store-home-page.component.spec.ts @@ -88,7 +88,7 @@ const pageOneItems: CatalogFeaturedItem[] = [ }, ]; -function createTenant(mainCarouselImages?: string[]): Tenant { +function createTenant(extras: Tenant['extras'] = {}): Tenant { return { id: 1, codigo: 'acme', @@ -102,8 +102,10 @@ function createTenant(mainCarouselImages?: string[]): Tenant { footer_bg_color: '#ffffff', header_logo: '/header.png', footer_logo: '/footer.png', + website_type_code: 'shopit', + website_type: { codigo: 'shopit', nombre: 'ShopIt' }, + extras, categories: [], - main_carousel_images: mainCarouselImages, }; } @@ -146,11 +148,8 @@ describe('StoreHomePageComponent', () => { ); }); - it('renders the tenant main carousel images at the beginning of the home page', async () => { - const mainCarouselImages = [ - 'https://example.com/carousel-1.webp', - 'https://example.com/carousel-2.webp', - ]; + it('renders the carousel URLs received in tenant extras', async () => { + const carousel = ['https://example.com/carousel-1.webp', 'https://example.com/carousel-2.webp']; await TestBed.configureTestingModule({ imports: [StoreHomePageComponent], @@ -162,7 +161,7 @@ describe('StoreHomePageComponent', () => { }, { provide: TenantService, - useValue: createTenantServiceStub(createTenant(mainCarouselImages)), + useValue: createTenantServiceStub(createTenant({ carousel })), }, ], }).compileComponents(); @@ -171,25 +170,29 @@ describe('StoreHomePageComponent', () => { fixture.detectChanges(); const element = fixture.nativeElement as HTMLElement; - const carousel = element.querySelector('app-main-carousel'); - const images = carousel?.querySelectorAll('.main-carousel__image'); + const carouselElement = element.querySelector('app-main-carousel'); + const images = carouselElement?.querySelectorAll('.main-carousel__image'); - expect(element.firstElementChild).toBe(carousel); + expect(element.firstElementChild).toBe(carouselElement); expect(images).toHaveLength(2); - expect(images?.[0].getAttribute('src')).toBe(mainCarouselImages[0]); - expect(images?.[1].getAttribute('src')).toBeNull(); - expect(element.querySelectorAll('app-product-column-with-image img')).toHaveLength(0); - - images?.[0].dispatchEvent(new Event('load')); - fixture.detectChanges(); - - expect(images?.[1].getAttribute('src')).toBe(mainCarouselImages[1]); - expect(element.querySelectorAll('app-product-column-with-image img')).toHaveLength(1); - - fixture.destroy(); + expect(images?.[0].getAttribute('src')).toBe(carousel[0]); }); - it('does not render the main carousel when the tenant has no images', async () => { + it('renders OnTicket hero and event configs received in tenant extras', async () => { + const tenant = createTenant({ + heroConfig: { + title_html: '

Fiesta Fútbol Infantil

', + background_image_id: 'https://example.com/hero.jpg', + }, + eventConfig: { + title: 'Fiesta Fútbol Infantil', + location: 'Rosario, Santa Fe', + dates: ['2026-12-05'], + }, + }); + tenant.website_type_code = 'onticket'; + tenant.website_type = { codigo: 'onticket', nombre: 'OnTicket' }; + await TestBed.configureTestingModule({ imports: [StoreHomePageComponent], providers: [ @@ -200,7 +203,7 @@ describe('StoreHomePageComponent', () => { }, { provide: TenantService, - useValue: createTenantServiceStub(createTenant([])), + useValue: createTenantServiceStub(tenant), }, ], }).compileComponents(); @@ -208,7 +211,14 @@ describe('StoreHomePageComponent', () => { const fixture = TestBed.createComponent(StoreHomePageComponent); fixture.detectChanges(); - expect((fixture.nativeElement as HTMLElement).querySelector('app-main-carousel')).toBeNull(); + const hero = (fixture.nativeElement as HTMLElement).querySelector( + 'app-hero-banner .hero-banner', + ) as HTMLElement; + + expect(hero).not.toBeNull(); + expect(hero.style.backgroundImage).toContain('https://example.com/hero.jpg'); + expect(hero.textContent).toContain('Fiesta Fútbol Infantil'); + expect(hero.textContent).toContain('Rosario, Santa Fe'); }); it('requests another page for the selected featured group', async () => { @@ -323,9 +333,9 @@ describe('StoreHomePageComponent', () => { it('adds a product-list cart event to the cart', async () => { const catalogServiceStub = { getCatalog: vi.fn(), getFeaturedGroupItems: vi.fn() }; const cartServiceStub = { - addItem: vi.fn().mockReturnValue( - of({ data: {}, message: 'Producto agregado correctamente' }), - ), + addItem: vi + .fn() + .mockReturnValue(of({ data: {}, message: 'Producto agregado correctamente' })), }; const toastServiceStub = { success: vi.fn(), danger: vi.fn() }; diff --git a/src/app/features/store/pages/store-home-page/store-home-page.component.ts b/src/app/features/store/pages/store-home-page/store-home-page.component.ts index bac1577..0723ab7 100644 --- a/src/app/features/store/pages/store-home-page/store-home-page.component.ts +++ b/src/app/features/store/pages/store-home-page/store-home-page.component.ts @@ -60,9 +60,10 @@ export class StoreHomePageComponent implements OnInit, OnDestroy { protected readonly error = signal(null); protected readonly mainCarouselReady = signal(false); protected readonly creatingDirectPurchase = signal(false); - protected readonly hasMainCarouselImages = computed( - () => (this.tenant()?.main_carousel_images?.length ?? 0) > 0, - ); + protected readonly mainCarouselImages = computed(() => this.tenant()?.extras?.carousel ?? []); + protected readonly heroConfig = computed(() => this.tenant()?.extras?.heroConfig ?? null); + protected readonly eventConfig = computed(() => this.tenant()?.extras?.eventConfig ?? null); + protected readonly hasMainCarouselImages = computed(() => this.mainCarouselImages().length > 0); private catalogRequestSubscription: Subscription | null = null; private readonly groupRequestSubscriptions = new Map(); @@ -109,20 +110,20 @@ export class StoreHomePageComponent implements OnInit, OnDestroy { .withCustomLoading() .getFeaturedGroupItems(groupId, { page }) .subscribe({ - next: (items) => { - this.catalog.update((groups) => - groups.map((candidate) => - candidate.id === groupId ? { ...candidate, items } : candidate, - ), - ); - this.error.set(null); - }, - error: () => { - this.error.set(STORE_HOME_PRODUCTS_ERROR_MESSAGE); - this.setGroupLoading(groupId, false); - }, - complete: () => this.setGroupLoading(groupId, false), - }); + next: (items) => { + this.catalog.update((groups) => + groups.map((candidate) => + candidate.id === groupId ? { ...candidate, items } : candidate, + ), + ); + this.error.set(null); + }, + error: () => { + this.error.set(STORE_HOME_PRODUCTS_ERROR_MESSAGE); + this.setGroupLoading(groupId, false); + }, + complete: () => this.setGroupLoading(groupId, false), + }); this.groupRequestSubscriptions.set(groupId, subscription); } @@ -189,14 +190,14 @@ export class StoreHomePageComponent implements OnInit, OnDestroy { .withCustomLoading() .getCatalog() .subscribe({ - next: (catalog) => this.catalog.set(catalog), - error: () => { - this.catalog.set([]); - this.loading.set(false); - this.error.set(STORE_HOME_PRODUCTS_ERROR_MESSAGE); - }, - complete: () => this.loading.set(false), - }); + next: (catalog) => this.catalog.set(catalog), + error: () => { + this.catalog.set([]); + this.loading.set(false); + this.error.set(STORE_HOME_PRODUCTS_ERROR_MESSAGE); + }, + complete: () => this.loading.set(false), + }); } private applyResolvedData(resolvedData: StoreHomeCatalogResolvedData): void { diff --git a/src/app/shared/components/hero-banner/hero-banner.component.html b/src/app/shared/components/hero-banner/hero-banner.component.html index e544c02..5ca582b 100644 --- a/src/app/shared/components/hero-banner/hero-banner.component.html +++ b/src/app/shared/components/hero-banner/hero-banner.component.html @@ -1,11 +1,16 @@
-
- @if (heroConfig) { -
+
@if (heroConfig.title_html) {
@@ -14,7 +19,10 @@
} @if (heroConfig.button_text) { - + {{ heroConfig.button_text }} @@ -30,8 +38,10 @@ @if (eventConfig.title) {

{{ eventConfig.title }}

} - -
+ +
@if (eventConfig.dates && eventConfig.dates.length > 0) {
diff --git a/src/app/shared/components/hero-banner/hero-banner.component.ts b/src/app/shared/components/hero-banner/hero-banner.component.ts index 54a75cd..c540993 100644 --- a/src/app/shared/components/hero-banner/hero-banner.component.ts +++ b/src/app/shared/components/hero-banner/hero-banner.component.ts @@ -1,7 +1,7 @@ import { Component, ChangeDetectionStrategy, Input } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule } from '@angular/router'; -import { HeroConfig, EventConfig } from '../../../core/services/tenant.interface'; +import { EventConfig, HeroConfig } from '../../../core/services/tenant.interface'; import { ButtonComponent } from '../button/button.component'; @Component({ @@ -20,20 +20,20 @@ export class HeroBannerComponent { if (!this.eventConfig?.dates || this.eventConfig.dates.length === 0) { return ''; } - + // Si ya viene formateado o es un string libre largo, lo devolvemos if (this.eventConfig.dates.length === 1 && this.eventConfig.dates[0].length > 10) { return this.eventConfig.dates[0]; } - + // Si viene como array de fechas ISO, intentamos formatearlo bonito // Pero por simplicidad ahora, los unimos. // Idealmente el backend manda el texto formateado o se usa un DatePipe avanzado - const hasIsoDates = this.eventConfig.dates.some(d => d.includes('-')); - + const hasIsoDates = this.eventConfig.dates.some((d) => d.includes('-')); + if (hasIsoDates) { - // Un simple join por si acaso, aunque lo ideal es que el admin ponga el texto tal cual - return '9, 10, 11 y 12 de Octubre 2026'; // Placeholder basado en los requerimientos, si no se envía como string formateado + // Un simple join por si acaso, aunque lo ideal es que el admin ponga el texto tal cual + return '9, 10, 11 y 12 de Octubre 2026'; // Placeholder basado en los requerimientos, si no se envía como string formateado } return this.eventConfig.dates.join(', ');