feat(account-page): implement dynamic account navigation menus in sidebar and user dropdown

This commit is contained in:
2026-07-24 09:58:18 -03:00
parent fa0e9de886
commit 974f947fc6
12 changed files with 243 additions and 60 deletions

View File

@@ -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()"
/> />
} }

View File

@@ -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>();

View File

@@ -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"

View File

@@ -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);
}
} }

View File

@@ -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>
} }

View File

@@ -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();

View File

@@ -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' },
], ],
}, },

View File

@@ -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);
}

View File

@@ -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>

View File

@@ -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',
]);
});
});

View File

@@ -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);
}
}

View File

@@ -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',