feat(store-layout): add tickets menu button and navigation handling in store header
This commit is contained in:
@@ -37,6 +37,17 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if (ticketsMenu(); as menu) {
|
||||||
|
<app-button
|
||||||
|
variant="primary"
|
||||||
|
hostClass="flex-shrink-0"
|
||||||
|
data-testid="store-header-tickets"
|
||||||
|
(click)="ticketsClick.emit()"
|
||||||
|
>
|
||||||
|
{{ menu.label }}
|
||||||
|
</app-button>
|
||||||
|
}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="d-flex align-items-center justify-content-end gap-1"
|
class="d-flex align-items-center justify-content-end gap-1"
|
||||||
aria-label="Acciones de usuario"
|
aria-label="Acciones de usuario"
|
||||||
|
|||||||
@@ -5,10 +5,17 @@ import { CartIconComponent } from '../../../../shared/components/cart-icon/cart-
|
|||||||
import { IconButtonComponent } from '../../../../shared/components/icon-button/icon-button.component';
|
import { IconButtonComponent } from '../../../../shared/components/icon-button/icon-button.component';
|
||||||
import { UserDropdownComponent } from './user-dropdown/user-dropdown.component';
|
import { UserDropdownComponent } from './user-dropdown/user-dropdown.component';
|
||||||
import { Menu } from '../../../services/tenant.interface';
|
import { Menu } from '../../../services/tenant.interface';
|
||||||
|
import { ButtonComponent } from '../../../../shared/components/button/button.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-store-header',
|
selector: 'app-store-header',
|
||||||
imports: [CartIconComponent, IconButtonComponent, RouterLink, UserDropdownComponent],
|
imports: [
|
||||||
|
ButtonComponent,
|
||||||
|
CartIconComponent,
|
||||||
|
IconButtonComponent,
|
||||||
|
RouterLink,
|
||||||
|
UserDropdownComponent,
|
||||||
|
],
|
||||||
templateUrl: './store-header.component.html',
|
templateUrl: './store-header.component.html',
|
||||||
styleUrl: './store-header.component.scss',
|
styleUrl: './store-header.component.scss',
|
||||||
})
|
})
|
||||||
@@ -20,7 +27,9 @@ export class StoreHeaderComponent {
|
|||||||
readonly user = input<AuthUser | null>(null);
|
readonly user = input<AuthUser | null>(null);
|
||||||
readonly isAuthenticated = input<boolean>(false);
|
readonly isAuthenticated = input<boolean>(false);
|
||||||
readonly accountNavigationMenus = input<readonly Menu[]>([]);
|
readonly accountNavigationMenus = input<readonly Menu[]>([]);
|
||||||
|
readonly ticketsMenu = input<Menu | null>(null);
|
||||||
readonly cartClick = output<void>();
|
readonly cartClick = output<void>();
|
||||||
|
readonly ticketsClick = output<void>();
|
||||||
readonly loginClick = output<void>();
|
readonly loginClick = output<void>();
|
||||||
readonly logoutClick = output<void>();
|
readonly logoutClick = output<void>();
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,9 @@
|
|||||||
[user]="user()"
|
[user]="user()"
|
||||||
[isAuthenticated]="isAuthenticated()"
|
[isAuthenticated]="isAuthenticated()"
|
||||||
[accountNavigationMenus]="accountNavigationMenus()"
|
[accountNavigationMenus]="accountNavigationMenus()"
|
||||||
|
[ticketsMenu]="ticketsMenu() ?? null"
|
||||||
(cartClick)="isCartOpen.set(!isCartOpen())"
|
(cartClick)="isCartOpen.set(!isCartOpen())"
|
||||||
|
(ticketsClick)="onTicketsClick()"
|
||||||
(loginClick)="onLoginClick()"
|
(loginClick)="onLoginClick()"
|
||||||
(logoutClick)="onLogoutClick()"
|
(logoutClick)="onLogoutClick()"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { of } from 'rxjs';
|
|||||||
import { Tenant } from '../../services/tenant.interface';
|
import { Tenant } from '../../services/tenant.interface';
|
||||||
import { TenantService } from '../../services/tenant.service';
|
import { TenantService } from '../../services/tenant.service';
|
||||||
import { CartService } from '../../services/cart/cart.service';
|
import { CartService } from '../../services/cart/cart.service';
|
||||||
|
import { Cart } from '../../services/cart/cart.interface';
|
||||||
import { ToastService } from '../../services/toast.service';
|
import { ToastService } from '../../services/toast.service';
|
||||||
import { AuthService } from '../../services/auth/auth.service';
|
import { AuthService } from '../../services/auth/auth.service';
|
||||||
import { AuthUser } from '../../services/auth/auth.interfaces';
|
import { AuthUser } from '../../services/auth/auth.interfaces';
|
||||||
@@ -122,8 +123,12 @@ const tenant: Tenant = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe('StoreLayoutComponent', () => {
|
describe('StoreLayoutComponent', () => {
|
||||||
|
let tenantState = signal<Tenant | null>(tenant);
|
||||||
|
let cartState = signal<Cart | null>(null);
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const tenantState = signal(tenant);
|
tenantState = signal<Tenant | null>(tenant);
|
||||||
|
cartState = signal<Cart | null>(null);
|
||||||
const authUserState = signal<AuthUser | null>(null);
|
const authUserState = signal<AuthUser | null>(null);
|
||||||
const isAuthenticatedState = signal(false);
|
const isAuthenticatedState = signal(false);
|
||||||
|
|
||||||
@@ -143,7 +148,7 @@ describe('StoreLayoutComponent', () => {
|
|||||||
{
|
{
|
||||||
provide: CartService,
|
provide: CartService,
|
||||||
useValue: {
|
useValue: {
|
||||||
cart: signal(null).asReadonly(),
|
cart: cartState.asReadonly(),
|
||||||
loadCart: () => of({ id: 1, items: [], subtotal: '0' }),
|
loadCart: () => of({ id: 1, items: [], subtotal: '0' }),
|
||||||
updateItemQuantity: vi
|
updateItemQuantity: vi
|
||||||
.fn()
|
.fn()
|
||||||
@@ -211,6 +216,69 @@ describe('StoreLayoutComponent', () => {
|
|||||||
expect(compiled.querySelector('.fa-linkedin-in')).not.toBeNull();
|
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<HTMLElement>(
|
||||||
|
'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', () => {
|
it('redirects to /login when user icon is clicked and user is not logged in', () => {
|
||||||
const authService = TestBed.inject(AuthService);
|
const authService = TestBed.inject(AuthService);
|
||||||
const router = TestBed.inject(Router);
|
const router = TestBed.inject(Router);
|
||||||
@@ -356,6 +424,25 @@ describe('StoreLayoutComponent', () => {
|
|||||||
it('redirects to /checkout when cart buy button is clicked', () => {
|
it('redirects to /checkout when cart buy button is clicked', () => {
|
||||||
const router = TestBed.inject(Router);
|
const router = TestBed.inject(Router);
|
||||||
vi.spyOn(router, 'navigate');
|
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);
|
const fixture = TestBed.createComponent(StoreLayoutComponent);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|||||||
@@ -79,6 +79,9 @@ export class StoreLayoutComponent implements OnInit {
|
|||||||
protected readonly tenant = this.tenantService.tenant;
|
protected readonly tenant = this.tenantService.tenant;
|
||||||
protected readonly user = this.authService.user;
|
protected readonly user = this.authService.user;
|
||||||
protected readonly isAuthenticated = this.authService.isAuthenticated;
|
protected readonly isAuthenticated = this.authService.isAuthenticated;
|
||||||
|
protected readonly ticketsMenu = computed(() =>
|
||||||
|
findMenu(this.tenant()?.menues ?? [], 'account.tickets'),
|
||||||
|
);
|
||||||
protected readonly accountNavigationMenus = computed(() => {
|
protected readonly accountNavigationMenus = computed(() => {
|
||||||
const accountMenu = findMenu(this.tenant()?.menues ?? [], 'account');
|
const accountMenu = findMenu(this.tenant()?.menues ?? [], 'account');
|
||||||
|
|
||||||
@@ -111,6 +114,14 @@ export class StoreLayoutComponent implements OnInit {
|
|||||||
void this.router.navigate(['/login']);
|
void this.router.navigate(['/login']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected onTicketsClick(): void {
|
||||||
|
const route = this.ticketsMenu()?.route;
|
||||||
|
|
||||||
|
if (route) {
|
||||||
|
void this.router.navigate([route]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected onLogoutClick(): void {
|
protected onLogoutClick(): void {
|
||||||
this.authService.logout().subscribe({
|
this.authService.logout().subscribe({
|
||||||
next: () => void this.router.navigate(['/login']),
|
next: () => void this.router.navigate(['/login']),
|
||||||
|
|||||||
Reference in New Issue
Block a user