feat(account-page): implement dynamic account navigation menus in sidebar and user dropdown
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
<header class="store-layout__header">
|
<header class="store-layout__header">
|
||||||
<div class="store-layout__header-main">
|
<div class="store-layout__header-main">
|
||||||
<div class="container-xl px-3 px-md-4 py-3 py-lg-4 d-flex flex-row flex-wrap align-items-center justify-content-between gap-3">
|
<div
|
||||||
|
class="container-xl px-3 px-md-4 py-3 py-lg-4 d-flex flex-row flex-wrap align-items-center justify-content-between gap-3"
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
routerLink="/"
|
routerLink="/"
|
||||||
class="store-layout__brand-slot d-inline-flex align-items-center gap-3 flex-shrink-0 text-decoration-none"
|
class="store-layout__brand-slot d-inline-flex align-items-center gap-3 flex-shrink-0 text-decoration-none"
|
||||||
@@ -16,7 +18,9 @@
|
|||||||
}
|
}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="d-flex flex-row align-items-center gap-3 ms-auto flex-nowrap flex-grow-1 justify-content-between justify-content-md-end">
|
<div
|
||||||
|
class="d-flex flex-row align-items-center gap-3 ms-auto flex-nowrap flex-grow-1 justify-content-between justify-content-md-end"
|
||||||
|
>
|
||||||
<div class="input-group store-layout__search" role="search">
|
<div class="input-group store-layout__search" role="search">
|
||||||
<label class="visually-hidden" for="store-search-input">Buscar productos</label>
|
<label class="visually-hidden" for="store-search-input">Buscar productos</label>
|
||||||
<input
|
<input
|
||||||
@@ -33,7 +37,10 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex align-items-center justify-content-end gap-1" aria-label="Acciones de usuario">
|
<div
|
||||||
|
class="d-flex align-items-center justify-content-end gap-1"
|
||||||
|
aria-label="Acciones de usuario"
|
||||||
|
>
|
||||||
<app-cart-icon
|
<app-cart-icon
|
||||||
[quantity]="cartQuantity()"
|
[quantity]="cartQuantity()"
|
||||||
ariaLabel="Carrito de compras"
|
ariaLabel="Carrito de compras"
|
||||||
@@ -54,6 +61,7 @@
|
|||||||
<app-user-dropdown
|
<app-user-dropdown
|
||||||
id="store-user-dropdown"
|
id="store-user-dropdown"
|
||||||
[user]="user()"
|
[user]="user()"
|
||||||
|
[accountNavigationMenus]="accountNavigationMenus()"
|
||||||
(logoutClick)="onLogoutClick()"
|
(logoutClick)="onLogoutClick()"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ import { AuthUser } from '../../../services/auth/auth.interfaces';
|
|||||||
import { CartIconComponent } from '../../../../shared/components/cart-icon/cart-icon.component';
|
import { CartIconComponent } from '../../../../shared/components/cart-icon/cart-icon.component';
|
||||||
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';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-store-header',
|
selector: 'app-store-header',
|
||||||
imports: [CartIconComponent, IconButtonComponent, RouterLink, UserDropdownComponent],
|
imports: [CartIconComponent, IconButtonComponent, RouterLink, UserDropdownComponent],
|
||||||
templateUrl: './store-header.component.html',
|
templateUrl: './store-header.component.html',
|
||||||
styleUrl: './store-header.component.scss'
|
styleUrl: './store-header.component.scss',
|
||||||
})
|
})
|
||||||
export class StoreHeaderComponent {
|
export class StoreHeaderComponent {
|
||||||
private readonly elementRef = inject(ElementRef<HTMLElement>);
|
private readonly elementRef = inject(ElementRef<HTMLElement>);
|
||||||
@@ -18,6 +19,7 @@ export class StoreHeaderComponent {
|
|||||||
readonly cartQuantity = input<number>(0);
|
readonly cartQuantity = input<number>(0);
|
||||||
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 cartClick = output<void>();
|
readonly cartClick = output<void>();
|
||||||
readonly loginClick = output<void>();
|
readonly loginClick = output<void>();
|
||||||
readonly logoutClick = output<void>();
|
readonly logoutClick = output<void>();
|
||||||
|
|||||||
@@ -5,12 +5,16 @@
|
|||||||
|
|
||||||
<div class="user-dropdown__divider"></div>
|
<div class="user-dropdown__divider"></div>
|
||||||
|
|
||||||
<a routerLink="/mi-cuenta/datos-personales" class="user-dropdown__item" role="menuitem" data-testid="user-dropdown-account">
|
@for (menu of accountNavigationMenus(); track menu.code) {
|
||||||
Mi cuenta
|
<a
|
||||||
</a>
|
[routerLink]="menu.route"
|
||||||
<a routerLink="/mi-cuenta/compras" class="user-dropdown__item" role="menuitem" data-testid="user-dropdown-purchases">
|
class="user-dropdown__item"
|
||||||
Mis compras
|
role="menuitem"
|
||||||
</a>
|
[attr.data-testid]="'user-dropdown-' + menu.code"
|
||||||
|
>
|
||||||
|
{{ menuLabel(menu) }}
|
||||||
|
</a>
|
||||||
|
}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="user-dropdown__item"
|
class="user-dropdown__item"
|
||||||
|
|||||||
@@ -1,17 +1,24 @@
|
|||||||
import { Component, computed, input, output } from '@angular/core';
|
import { Component, computed, input, output } from '@angular/core';
|
||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
import { AuthUser } from '../../../../services/auth/auth.interfaces';
|
import { AuthUser } from '../../../../services/auth/auth.interfaces';
|
||||||
|
import { getAccountNavigationLabel } from '../../../../services/menu.utils';
|
||||||
|
import { Menu } from '../../../../services/tenant.interface';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-user-dropdown',
|
selector: 'app-user-dropdown',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [RouterLink],
|
imports: [RouterLink],
|
||||||
templateUrl: './user-dropdown.component.html',
|
templateUrl: './user-dropdown.component.html',
|
||||||
styleUrl: './user-dropdown.component.scss'
|
styleUrl: './user-dropdown.component.scss',
|
||||||
})
|
})
|
||||||
export class UserDropdownComponent {
|
export class UserDropdownComponent {
|
||||||
readonly user = input<AuthUser | null>(null);
|
readonly user = input<AuthUser | null>(null);
|
||||||
|
readonly accountNavigationMenus = input<readonly Menu[]>([]);
|
||||||
readonly logoutClick = output<void>();
|
readonly logoutClick = output<void>();
|
||||||
|
|
||||||
protected readonly displayName = computed(() => this.user()?.nombre_apellido || 'Mi cuenta');
|
protected readonly displayName = computed(() => this.user()?.nombre_apellido || 'Mi cuenta');
|
||||||
|
|
||||||
|
protected menuLabel(menu: Menu): string {
|
||||||
|
return getAccountNavigationLabel(menu);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
[logoUrl]="tenant()?.header_logo ?? null"
|
[logoUrl]="tenant()?.header_logo ?? null"
|
||||||
[user]="user()"
|
[user]="user()"
|
||||||
[isAuthenticated]="isAuthenticated()"
|
[isAuthenticated]="isAuthenticated()"
|
||||||
|
[accountNavigationMenus]="accountNavigationMenus()"
|
||||||
(cartClick)="isCartOpen.set(!isCartOpen())"
|
(cartClick)="isCartOpen.set(!isCartOpen())"
|
||||||
(loginClick)="onLoginClick()"
|
(loginClick)="onLoginClick()"
|
||||||
(logoutClick)="onLogoutClick()"
|
(logoutClick)="onLogoutClick()"
|
||||||
@@ -21,8 +22,12 @@
|
|||||||
[backgroundColor]="'#ffffff'"
|
[backgroundColor]="'#ffffff'"
|
||||||
(closed)="isCartOpen.set(false)"
|
(closed)="isCartOpen.set(false)"
|
||||||
>
|
>
|
||||||
<app-button variant="secondary" class="flex-grow-1" (click)="isCartOpen.set(false)">Seguir comprando</app-button>
|
<app-button variant="secondary" class="flex-grow-1" (click)="isCartOpen.set(false)"
|
||||||
<app-button variant="primary" class="flex-grow-1" (click)="onCheckoutClick()">Comprar</app-button>
|
>Seguir comprando</app-button
|
||||||
|
>
|
||||||
|
<app-button variant="primary" class="flex-grow-1" (click)="onCheckoutClick()"
|
||||||
|
>Comprar</app-button
|
||||||
|
>
|
||||||
</app-cart>
|
</app-cart>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,39 @@ const tenant: Tenant = {
|
|||||||
header_logo: 'https://example.com/header.png',
|
header_logo: 'https://example.com/header.png',
|
||||||
footer_logo: 'https://example.com/footer.png',
|
footer_logo: 'https://example.com/footer.png',
|
||||||
menues: [
|
menues: [
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
code: 'account',
|
||||||
|
parent_menu_code: null,
|
||||||
|
content_type: 'dynamic',
|
||||||
|
route: '/mi-cuenta',
|
||||||
|
submenues: [
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
code: 'profile',
|
||||||
|
parent_menu_code: 'account',
|
||||||
|
content_type: 'dynamic',
|
||||||
|
route: '/mi-cuenta/datos-personales',
|
||||||
|
submenues: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
code: 'purchases',
|
||||||
|
parent_menu_code: 'account',
|
||||||
|
content_type: 'dynamic',
|
||||||
|
route: '/mi-cuenta/compras',
|
||||||
|
submenues: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
code: 'tickets',
|
||||||
|
parent_menu_code: 'account',
|
||||||
|
content_type: 'dynamic',
|
||||||
|
route: '/mi-cuenta/tickets',
|
||||||
|
submenues: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
code: 'help',
|
code: 'help',
|
||||||
@@ -97,17 +130,19 @@ describe('StoreLayoutComponent', () => {
|
|||||||
tenant: tenantState.asReadonly(),
|
tenant: tenantState.asReadonly(),
|
||||||
status: signal<'ready'>('ready').asReadonly(),
|
status: signal<'ready'>('ready').asReadonly(),
|
||||||
getTenant: () => tenantState(),
|
getTenant: () => tenantState(),
|
||||||
bootstrap: vi.fn().mockResolvedValue(undefined)
|
bootstrap: vi.fn().mockResolvedValue(undefined),
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: CartService,
|
provide: CartService,
|
||||||
useValue: {
|
useValue: {
|
||||||
cart: signal(null).asReadonly(),
|
cart: signal(null).asReadonly(),
|
||||||
loadCart: () => of({ id: 1, items: [], subtotal: '0' }),
|
loadCart: () => of({ id: 1, items: [], subtotal: '0' }),
|
||||||
updateItemQuantity: vi.fn().mockReturnValue(of({ data: { id: 1, items: [], subtotal: '0' } })),
|
updateItemQuantity: vi
|
||||||
removeItem: vi.fn().mockReturnValue(of({ data: { id: 1, items: [], subtotal: '0' } }))
|
.fn()
|
||||||
}
|
.mockReturnValue(of({ data: { id: 1, items: [], subtotal: '0' } })),
|
||||||
|
removeItem: vi.fn().mockReturnValue(of({ data: { id: 1, items: [], subtotal: '0' } })),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: ToastService,
|
provide: ToastService,
|
||||||
@@ -116,18 +151,18 @@ describe('StoreLayoutComponent', () => {
|
|||||||
info: vi.fn(),
|
info: vi.fn(),
|
||||||
danger: vi.fn(),
|
danger: vi.fn(),
|
||||||
success: vi.fn(),
|
success: vi.fn(),
|
||||||
dismiss: vi.fn()
|
dismiss: vi.fn(),
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: AuthService,
|
provide: AuthService,
|
||||||
useValue: {
|
useValue: {
|
||||||
user: authUserState,
|
user: authUserState,
|
||||||
isAuthenticated: isAuthenticatedState,
|
isAuthenticated: isAuthenticatedState,
|
||||||
logout: vi.fn().mockReturnValue(of(void 0))
|
logout: vi.fn().mockReturnValue(of(void 0)),
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -152,10 +187,14 @@ describe('StoreLayoutComponent', () => {
|
|||||||
expect(compiled.querySelector('[data-testid="store-header-brand-slot"]')).not.toBeNull();
|
expect(compiled.querySelector('[data-testid="store-header-brand-slot"]')).not.toBeNull();
|
||||||
expect(compiled.querySelector('[data-testid="store-footer-brand-slot"]')).not.toBeNull();
|
expect(compiled.querySelector('[data-testid="store-footer-brand-slot"]')).not.toBeNull();
|
||||||
expect(
|
expect(
|
||||||
compiled.querySelector(`app-store-header img.store-layout__brand-logo[src="${tenant.header_logo}"]`)
|
compiled.querySelector(
|
||||||
|
`app-store-header img.store-layout__brand-logo[src="${tenant.header_logo}"]`,
|
||||||
|
),
|
||||||
).not.toBeNull();
|
).not.toBeNull();
|
||||||
expect(
|
expect(
|
||||||
compiled.querySelector(`app-store-footer img.store-layout__brand-logo[src="${tenant.footer_logo}"]`)
|
compiled.querySelector(
|
||||||
|
`app-store-footer img.store-layout__brand-logo[src="${tenant.footer_logo}"]`,
|
||||||
|
),
|
||||||
).not.toBeNull();
|
).not.toBeNull();
|
||||||
expect(compiled.querySelector('.fa-cart-shopping')).not.toBeNull();
|
expect(compiled.querySelector('.fa-cart-shopping')).not.toBeNull();
|
||||||
expect(compiled.querySelector('.fa-circle-user')).not.toBeNull();
|
expect(compiled.querySelector('.fa-circle-user')).not.toBeNull();
|
||||||
@@ -176,7 +215,9 @@ describe('StoreLayoutComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const compiled = fixture.nativeElement as HTMLElement;
|
const compiled = fixture.nativeElement as HTMLElement;
|
||||||
const userButton = compiled.querySelector('app-icon-button[variant="user"] button') as HTMLButtonElement;
|
const userButton = compiled.querySelector(
|
||||||
|
'app-icon-button[variant="user"] button',
|
||||||
|
) as HTMLButtonElement;
|
||||||
expect(userButton).not.toBeNull();
|
expect(userButton).not.toBeNull();
|
||||||
userButton.click();
|
userButton.click();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@@ -194,20 +235,25 @@ describe('StoreLayoutComponent', () => {
|
|||||||
(authService.user as any).set({
|
(authService.user as any).set({
|
||||||
id: 1,
|
id: 1,
|
||||||
nombre_apellido: 'Ada Lovelace',
|
nombre_apellido: 'Ada Lovelace',
|
||||||
email: 'ada@example.com'
|
email: 'ada@example.com',
|
||||||
});
|
});
|
||||||
|
|
||||||
const fixture = TestBed.createComponent(StoreLayoutComponent);
|
const fixture = TestBed.createComponent(StoreLayoutComponent);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const compiled = fixture.nativeElement as HTMLElement;
|
const compiled = fixture.nativeElement as HTMLElement;
|
||||||
const userButton = compiled.querySelector('app-icon-button[variant="user"] button') as HTMLButtonElement;
|
const userButton = compiled.querySelector(
|
||||||
|
'app-icon-button[variant="user"] button',
|
||||||
|
) as HTMLButtonElement;
|
||||||
expect(userButton).not.toBeNull();
|
expect(userButton).not.toBeNull();
|
||||||
userButton.click();
|
userButton.click();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
expect(compiled.querySelector('app-user-dropdown')).not.toBeNull();
|
expect(compiled.querySelector('app-user-dropdown')).not.toBeNull();
|
||||||
expect(compiled.textContent).toContain('Ada Lovelace');
|
expect(compiled.textContent).toContain('Ada Lovelace');
|
||||||
|
expect(compiled.querySelector('[data-testid="user-dropdown-profile"]')).not.toBeNull();
|
||||||
|
expect(compiled.querySelector('[data-testid="user-dropdown-purchases"]')).not.toBeNull();
|
||||||
|
expect(compiled.querySelector('[data-testid="user-dropdown-tickets"]')).toBeNull();
|
||||||
expect(router.navigate).not.toHaveBeenCalled();
|
expect(router.navigate).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -220,18 +266,22 @@ describe('StoreLayoutComponent', () => {
|
|||||||
(authService.user as any).set({
|
(authService.user as any).set({
|
||||||
id: 1,
|
id: 1,
|
||||||
nombre_apellido: 'Ada Lovelace',
|
nombre_apellido: 'Ada Lovelace',
|
||||||
email: 'ada@example.com'
|
email: 'ada@example.com',
|
||||||
});
|
});
|
||||||
|
|
||||||
const fixture = TestBed.createComponent(StoreLayoutComponent);
|
const fixture = TestBed.createComponent(StoreLayoutComponent);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const compiled = fixture.nativeElement as HTMLElement;
|
const compiled = fixture.nativeElement as HTMLElement;
|
||||||
const userButton = compiled.querySelector('app-icon-button[variant="user"] button') as HTMLButtonElement;
|
const userButton = compiled.querySelector(
|
||||||
|
'app-icon-button[variant="user"] button',
|
||||||
|
) as HTMLButtonElement;
|
||||||
userButton.click();
|
userButton.click();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const logoutButton = compiled.querySelector('[data-testid="user-dropdown-logout"]') as HTMLButtonElement;
|
const logoutButton = compiled.querySelector(
|
||||||
|
'[data-testid="user-dropdown-logout"]',
|
||||||
|
) as HTMLButtonElement;
|
||||||
expect(logoutButton).not.toBeNull();
|
expect(logoutButton).not.toBeNull();
|
||||||
logoutButton.click();
|
logoutButton.click();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@@ -252,14 +302,16 @@ describe('StoreLayoutComponent', () => {
|
|||||||
const footerLinks = Array.from(
|
const footerLinks = Array.from(
|
||||||
compiled.querySelectorAll<HTMLAnchorElement>('app-store-footer a.store-layout__footer-link'),
|
compiled.querySelectorAll<HTMLAnchorElement>('app-store-footer a.store-layout__footer-link'),
|
||||||
);
|
);
|
||||||
const accountLink = footerLinks.find((link) => link.textContent?.trim() === 'Mi cuenta');
|
const profileLink = footerLinks.find((link) => link.textContent?.trim() === 'Mi cuenta');
|
||||||
const purchasesLink = footerLinks.find((link) => link.textContent?.trim() === 'Mis compras');
|
const purchasesLink = footerLinks.find((link) => link.textContent?.trim() === 'Mis compras');
|
||||||
|
const ticketsLink = footerLinks.find((link) => link.textContent?.trim() === 'Mis tickets');
|
||||||
const logoutButton = Array.from(
|
const logoutButton = Array.from(
|
||||||
compiled.querySelectorAll<HTMLButtonElement>('app-store-footer button'),
|
compiled.querySelectorAll<HTMLButtonElement>('app-store-footer button'),
|
||||||
).find((button) => button.textContent?.trim() === 'Cerrar sesion');
|
).find((button) => button.textContent?.trim() === 'Cerrar sesion');
|
||||||
|
|
||||||
expect(accountLink?.getAttribute('href')).toBe('/mi-cuenta/datos-personales');
|
expect(profileLink?.getAttribute('href')).toBe('/mi-cuenta/datos-personales');
|
||||||
expect(purchasesLink?.getAttribute('href')).toBe('/mi-cuenta/compras');
|
expect(purchasesLink?.getAttribute('href')).toBe('/mi-cuenta/compras');
|
||||||
|
expect(ticketsLink).toBeUndefined();
|
||||||
expect(logoutButton).toBeDefined();
|
expect(logoutButton).toBeDefined();
|
||||||
|
|
||||||
logoutButton!.click();
|
logoutButton!.click();
|
||||||
@@ -275,11 +327,11 @@ describe('StoreLayoutComponent', () => {
|
|||||||
|
|
||||||
const compiled = fixture.nativeElement as HTMLElement;
|
const compiled = fixture.nativeElement as HTMLElement;
|
||||||
const footerLinks = Array.from(
|
const footerLinks = Array.from(
|
||||||
compiled.querySelectorAll<HTMLAnchorElement>(
|
compiled.querySelectorAll<HTMLAnchorElement>('app-store-footer a.store-layout__footer-link'),
|
||||||
'app-store-footer a.store-layout__footer-link',
|
);
|
||||||
),
|
const helpLinks = footerLinks.filter((link) =>
|
||||||
|
link.getAttribute('href')?.startsWith('/ayuda/'),
|
||||||
);
|
);
|
||||||
const helpLinks = footerLinks.filter((link) => link.getAttribute('href')?.startsWith('/ayuda/'));
|
|
||||||
|
|
||||||
expect(helpLinks.map((link) => link.textContent?.trim())).toEqual([
|
expect(helpLinks.map((link) => link.textContent?.trim())).toEqual([
|
||||||
'Contacto',
|
'Contacto',
|
||||||
@@ -303,8 +355,9 @@ describe('StoreLayoutComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const compiled = fixture.nativeElement as HTMLElement;
|
const compiled = fixture.nativeElement as HTMLElement;
|
||||||
const buyButton = Array.from(compiled.querySelectorAll('app-button button'))
|
const buyButton = Array.from(compiled.querySelectorAll('app-button button')).find(
|
||||||
.find((button) => button.textContent?.trim() === 'Comprar') as HTMLButtonElement | undefined;
|
(button) => button.textContent?.trim() === 'Comprar',
|
||||||
|
) as HTMLButtonElement | undefined;
|
||||||
|
|
||||||
expect(buyButton).toBeDefined();
|
expect(buyButton).toBeDefined();
|
||||||
buyButton!.click();
|
buyButton!.click();
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { CartComponent, CartItemMock } from '../../../shared/components/cart/car
|
|||||||
import { ButtonComponent } from '../../../shared/components/button/button.component';
|
import { ButtonComponent } from '../../../shared/components/button/button.component';
|
||||||
import { CartItem } from '../../services/cart/cart.interface';
|
import { CartItem } from '../../services/cart/cart.interface';
|
||||||
import { AuthService } from '../../services/auth/auth.service';
|
import { AuthService } from '../../services/auth/auth.service';
|
||||||
import { findMenu, getMenuLabel } from '../../services/menu.utils';
|
import { findMenu, getAccountNavigationLabel, getMenuLabel } from '../../services/menu.utils';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-store-layout',
|
selector: 'app-store-layout',
|
||||||
@@ -79,6 +79,11 @@ 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 accountNavigationMenus = computed(() => {
|
||||||
|
const accountSubmenues = findMenu(this.tenant()?.menues ?? [], 'account')?.submenues ?? [];
|
||||||
|
|
||||||
|
return accountSubmenues.filter((menu) => ['profile', 'purchases'].includes(menu.code));
|
||||||
|
});
|
||||||
|
|
||||||
protected readonly cartQuantity = computed(() => {
|
protected readonly cartQuantity = computed(() => {
|
||||||
const cart = this.cartService.cart();
|
const cart = this.cartService.cart();
|
||||||
@@ -109,15 +114,16 @@ export class StoreLayoutComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected readonly footerSections = computed<StoreFooterSection[]>(() => {
|
protected readonly footerSections = computed<StoreFooterSection[]>(() => {
|
||||||
const helpSubmenues =
|
const helpSubmenues = findMenu(this.tenant()?.menues ?? [], 'help')?.submenues ?? [];
|
||||||
findMenu(this.tenant()?.menues ?? [], 'help')?.submenues ?? [];
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
heading: 'Cuenta',
|
heading: 'Cuenta',
|
||||||
links: [
|
links: [
|
||||||
{ label: 'Mi cuenta', routerLink: '/mi-cuenta/datos-personales' },
|
...this.accountNavigationMenus().map((menu) => ({
|
||||||
{ label: 'Mis compras', routerLink: '/mi-cuenta/compras' },
|
label: getAccountNavigationLabel(menu),
|
||||||
|
routerLink: menu.route,
|
||||||
|
})),
|
||||||
{ label: 'Cerrar sesion', action: 'logout' },
|
{ label: 'Cerrar sesion', action: 'logout' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { Menu } from './tenant.interface';
|
import { Menu } from './tenant.interface';
|
||||||
|
|
||||||
const MENU_LABELS: Record<string, string> = {
|
const MENU_LABELS: Record<string, string> = {
|
||||||
|
account: 'Mi cuenta',
|
||||||
|
profile: 'Datos personales',
|
||||||
|
purchases: 'Mis compras',
|
||||||
|
tickets: 'Mis tickets',
|
||||||
'help.faq': 'Preguntas frecuentes',
|
'help.faq': 'Preguntas frecuentes',
|
||||||
'help.contact': 'Contacto',
|
'help.contact': 'Contacto',
|
||||||
'help.payment-methods': 'Medios de pago',
|
'help.payment-methods': 'Medios de pago',
|
||||||
@@ -35,3 +39,7 @@ export function getMenuLabel(menu: Menu): string {
|
|||||||
|
|
||||||
return label.charAt(0).toUpperCase() + label.slice(1);
|
return label.charAt(0).toUpperCase() + label.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getAccountNavigationLabel(menu: Menu): string {
|
||||||
|
return menu.code === 'profile' ? 'Mi cuenta' : getMenuLabel(menu);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<div class="sidebar-container">
|
<div class="sidebar-container">
|
||||||
<h2 class="sidebar-title">MI CUENTA</h2>
|
<h2 class="sidebar-title">MI CUENTA</h2>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<nav class="sidebar-nav">
|
<nav class="sidebar-nav" aria-label="Secciones de mi cuenta">
|
||||||
<a routerLink="/mi-cuenta/datos-personales" routerLinkActive="active" class="nav-link"
|
@for (menu of submenues(); track menu.code) {
|
||||||
>Datos Personales</a
|
<a [routerLink]="menu.route" routerLinkActive="active" class="nav-link">
|
||||||
>
|
{{ menuLabel(menu) }}
|
||||||
<a routerLink="/mi-cuenta/compras" routerLinkActive="active" class="nav-link">Mis compras</a>
|
</a>
|
||||||
<a routerLink="/mi-cuenta/tickets" routerLinkActive="active" class="nav-link">Mis tickets</a>
|
}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { signal } from '@angular/core';
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { provideRouter } from '@angular/router';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { Tenant } from '../../../../../../core/services/tenant.interface';
|
||||||
|
import { TenantService } from '../../../../../../core/services/tenant.service';
|
||||||
|
import { AccountSidebar } from './account-sidebar';
|
||||||
|
|
||||||
|
describe('AccountSidebar', () => {
|
||||||
|
it('renders only the account submenus configured for the tenant', async () => {
|
||||||
|
const tenant = signal<Tenant>({
|
||||||
|
id: 1,
|
||||||
|
codigo: 'test',
|
||||||
|
nombre: 'Test',
|
||||||
|
dominio: 'localhost',
|
||||||
|
primary_color: '#000000',
|
||||||
|
secondary_color: '#000000',
|
||||||
|
danger_color: '#000000',
|
||||||
|
success_color: '#000000',
|
||||||
|
header_bg_color: '#ffffff',
|
||||||
|
footer_bg_color: '#ffffff',
|
||||||
|
header_logo: '',
|
||||||
|
footer_logo: '',
|
||||||
|
menues: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
code: 'account',
|
||||||
|
parent_menu_code: null,
|
||||||
|
content_type: 'dynamic',
|
||||||
|
route: '/mi-cuenta',
|
||||||
|
submenues: [
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
code: 'profile',
|
||||||
|
parent_menu_code: 'account',
|
||||||
|
content_type: 'dynamic',
|
||||||
|
route: '/mi-cuenta/datos-personales',
|
||||||
|
submenues: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
code: 'purchases',
|
||||||
|
parent_menu_code: 'account',
|
||||||
|
content_type: 'dynamic',
|
||||||
|
route: '/mi-cuenta/compras',
|
||||||
|
submenues: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [AccountSidebar],
|
||||||
|
providers: [
|
||||||
|
provideRouter([]),
|
||||||
|
{
|
||||||
|
provide: TenantService,
|
||||||
|
useValue: { tenant: tenant.asReadonly() },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(AccountSidebar);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const element = fixture.nativeElement as HTMLElement;
|
||||||
|
const links = Array.from(element.querySelectorAll<HTMLAnchorElement>('.nav-link'));
|
||||||
|
|
||||||
|
expect(links.map((link) => link.textContent?.trim())).toEqual([
|
||||||
|
'Datos personales',
|
||||||
|
'Mis compras',
|
||||||
|
]);
|
||||||
|
expect(links.map((link) => link.getAttribute('href'))).toEqual([
|
||||||
|
'/mi-cuenta/datos-personales',
|
||||||
|
'/mi-cuenta/compras',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, computed, inject } from '@angular/core';
|
||||||
import { RouterLink, RouterLinkActive } from '@angular/router';
|
import { RouterLink, RouterLinkActive } from '@angular/router';
|
||||||
|
|
||||||
|
import { findMenu, getMenuLabel } from '../../../../../../core/services/menu.utils';
|
||||||
|
import { Menu } from '../../../../../../core/services/tenant.interface';
|
||||||
|
import { TenantService } from '../../../../../../core/services/tenant.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-account-sidebar',
|
selector: 'app-account-sidebar',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@@ -8,4 +12,14 @@ import { RouterLink, RouterLinkActive } from '@angular/router';
|
|||||||
templateUrl: './account-sidebar.html',
|
templateUrl: './account-sidebar.html',
|
||||||
styleUrl: './account-sidebar.scss',
|
styleUrl: './account-sidebar.scss',
|
||||||
})
|
})
|
||||||
export class AccountSidebar {}
|
export class AccountSidebar {
|
||||||
|
private readonly tenantService = inject(TenantService);
|
||||||
|
|
||||||
|
protected readonly submenues = computed(
|
||||||
|
() => findMenu(this.tenantService.tenant()?.menues ?? [], 'account')?.submenues ?? [],
|
||||||
|
);
|
||||||
|
|
||||||
|
protected menuLabel(menu: Menu): string {
|
||||||
|
return getMenuLabel(menu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -89,9 +89,7 @@ export const routes: Routes = [
|
|||||||
path: 'preguntas-frecuentes',
|
path: 'preguntas-frecuentes',
|
||||||
canActivate: [hasMenuGuard('help.faq')],
|
canActivate: [hasMenuGuard('help.faq')],
|
||||||
loadComponent: () =>
|
loadComponent: () =>
|
||||||
import('./pages/help-page/pages/faq-page/faq-page').then(
|
import('./pages/help-page/pages/faq-page/faq-page').then((m) => m.FaqPage),
|
||||||
(m) => m.FaqPage,
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'contacto',
|
path: 'contacto',
|
||||||
@@ -117,11 +115,9 @@ export const routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'mi-cuenta',
|
path: 'mi-cuenta',
|
||||||
canActivate: [authGuard],
|
canActivate: [authGuard, hasMenuGuard('account')],
|
||||||
loadComponent: () =>
|
loadComponent: () =>
|
||||||
import('./pages/account-page/sidebar-layout/sidebar-layout').then(
|
import('./pages/account-page/sidebar-layout/sidebar-layout').then((m) => m.SidebarLayout),
|
||||||
(m) => m.SidebarLayout,
|
|
||||||
),
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'datos-personales',
|
path: 'datos-personales',
|
||||||
|
|||||||
Reference in New Issue
Block a user