From 34d351afc50f0a5d6ca0ff8e1d62f9216bb16117 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Fri, 24 Jul 2026 10:42:09 -0300 Subject: [PATCH] feat(store-layout): add tickets menu button and navigation handling in store header --- .../store-header/store-header.component.html | 11 +++ .../store-header/store-header.component.ts | 11 ++- .../store-layout/store-layout.component.html | 2 + .../store-layout.component.spec.ts | 91 ++++++++++++++++++- .../store-layout/store-layout.component.ts | 11 +++ 5 files changed, 123 insertions(+), 3 deletions(-) diff --git a/src/app/core/layout/store-layout/store-header/store-header.component.html b/src/app/core/layout/store-layout/store-header/store-header.component.html index 3e36778..b2f2ac5 100644 --- a/src/app/core/layout/store-layout/store-header/store-header.component.html +++ b/src/app/core/layout/store-layout/store-header/store-header.component.html @@ -37,6 +37,17 @@ + @if (ticketsMenu(); as menu) { + + {{ menu.label }} + + } +
(null); readonly isAuthenticated = input(false); readonly accountNavigationMenus = input([]); + readonly ticketsMenu = input(null); readonly cartClick = output(); + readonly ticketsClick = output(); readonly loginClick = output(); readonly logoutClick = output(); diff --git a/src/app/core/layout/store-layout/store-layout.component.html b/src/app/core/layout/store-layout/store-layout.component.html index 21d1d8e..ab98f43 100644 --- a/src/app/core/layout/store-layout/store-layout.component.html +++ b/src/app/core/layout/store-layout/store-layout.component.html @@ -5,7 +5,9 @@ [user]="user()" [isAuthenticated]="isAuthenticated()" [accountNavigationMenus]="accountNavigationMenus()" + [ticketsMenu]="ticketsMenu() ?? null" (cartClick)="isCartOpen.set(!isCartOpen())" + (ticketsClick)="onTicketsClick()" (loginClick)="onLoginClick()" (logoutClick)="onLogoutClick()" /> diff --git a/src/app/core/layout/store-layout/store-layout.component.spec.ts b/src/app/core/layout/store-layout/store-layout.component.spec.ts index ce606fe..dbf1ed0 100644 --- a/src/app/core/layout/store-layout/store-layout.component.spec.ts +++ b/src/app/core/layout/store-layout/store-layout.component.spec.ts @@ -8,6 +8,7 @@ import { of } from 'rxjs'; import { Tenant } from '../../services/tenant.interface'; import { TenantService } from '../../services/tenant.service'; import { CartService } from '../../services/cart/cart.service'; +import { Cart } from '../../services/cart/cart.interface'; import { ToastService } from '../../services/toast.service'; import { AuthService } from '../../services/auth/auth.service'; import { AuthUser } from '../../services/auth/auth.interfaces'; @@ -122,8 +123,12 @@ const tenant: Tenant = { }; describe('StoreLayoutComponent', () => { + let tenantState = signal(tenant); + let cartState = signal(null); + beforeEach(async () => { - const tenantState = signal(tenant); + tenantState = signal(tenant); + cartState = signal(null); const authUserState = signal(null); const isAuthenticatedState = signal(false); @@ -143,7 +148,7 @@ describe('StoreLayoutComponent', () => { { provide: CartService, useValue: { - cart: signal(null).asReadonly(), + cart: cartState.asReadonly(), loadCart: () => of({ id: 1, items: [], subtotal: '0' }), updateItemQuantity: vi .fn() @@ -211,6 +216,69 @@ describe('StoreLayoutComponent', () => { expect(compiled.querySelector('.fa-linkedin-in')).not.toBeNull(); }); + it('renders the primary tickets action when the tenant has the tickets menu', () => { + const router = TestBed.inject(Router); + vi.spyOn(router, 'navigate'); + const configuredTicketsLabel = 'Entradas digitales'; + tenantState.set({ + ...tenant, + menues: tenant.menues?.map((menu) => + menu.code === 'account' + ? { + ...menu, + submenues: menu.submenues.map((submenu) => + submenu.code === 'account.tickets' + ? { ...submenu, label: configuredTicketsLabel } + : submenu, + ), + } + : menu, + ), + }); + + const fixture = TestBed.createComponent(StoreLayoutComponent); + fixture.detectChanges(); + + const ticketsAction = (fixture.nativeElement as HTMLElement).querySelector( + 'app-button[data-testid="store-header-tickets"]', + ); + const ticketsButton = ticketsAction?.querySelector('button'); + + expect(ticketsAction).not.toBeNull(); + expect(ticketsButton).not.toBeNull(); + expect(ticketsButton?.textContent?.trim()).toBe(configuredTicketsLabel); + expect(ticketsButton?.classList).toContain('btn-primary'); + + ticketsButton?.click(); + + expect(router.navigate).toHaveBeenCalledWith(['/mi-cuenta/tickets']); + }); + + it('does not render the tickets action when the tenant lacks the tickets menu', () => { + tenantState.set({ + ...tenant, + menues: tenant.menues?.map((menu) => + menu.code === 'account' + ? { + ...menu, + submenues: menu.submenues.filter( + (submenu) => submenu.code !== 'account.tickets', + ), + } + : menu, + ), + }); + + const fixture = TestBed.createComponent(StoreLayoutComponent); + fixture.detectChanges(); + + expect( + (fixture.nativeElement as HTMLElement).querySelector( + '[data-testid="store-header-tickets"]', + ), + ).toBeNull(); + }); + it('redirects to /login when user icon is clicked and user is not logged in', () => { const authService = TestBed.inject(AuthService); const router = TestBed.inject(Router); @@ -356,6 +424,25 @@ describe('StoreLayoutComponent', () => { it('redirects to /checkout when cart buy button is clicked', () => { const router = TestBed.inject(Router); vi.spyOn(router, 'navigate'); + cartState.set({ + id: 1, + tenant_codigo: tenant.codigo, + status: 'active', + subtotal: '100.00', + items: [ + { + id: 1, + cantidad: 1, + precio_unitario: '100.00', + catalog_item_id: 1, + variant_id: null, + product: { + nombre: 'Producto', + imagen: null, + }, + }, + ], + }); const fixture = TestBed.createComponent(StoreLayoutComponent); fixture.detectChanges(); diff --git a/src/app/core/layout/store-layout/store-layout.component.ts b/src/app/core/layout/store-layout/store-layout.component.ts index f89bb39..027aaf6 100644 --- a/src/app/core/layout/store-layout/store-layout.component.ts +++ b/src/app/core/layout/store-layout/store-layout.component.ts @@ -79,6 +79,9 @@ export class StoreLayoutComponent implements OnInit { protected readonly tenant = this.tenantService.tenant; protected readonly user = this.authService.user; protected readonly isAuthenticated = this.authService.isAuthenticated; + protected readonly ticketsMenu = computed(() => + findMenu(this.tenant()?.menues ?? [], 'account.tickets'), + ); protected readonly accountNavigationMenus = computed(() => { const accountMenu = findMenu(this.tenant()?.menues ?? [], 'account'); @@ -111,6 +114,14 @@ export class StoreLayoutComponent implements OnInit { void this.router.navigate(['/login']); } + protected onTicketsClick(): void { + const route = this.ticketsMenu()?.route; + + if (route) { + void this.router.navigate([route]); + } + } + protected onLogoutClick(): void { this.authService.logout().subscribe({ next: () => void this.router.navigate(['/login']),