Compare commits
5 Commits
2b3e054ccf
...
feature/re
| Author | SHA1 | Date | |
|---|---|---|---|
| 50f986dd2f | |||
| 393aeec7a5 | |||
| a012f94198 | |||
| 6b9087262a | |||
| 4b98df4058 |
@@ -0,0 +1,15 @@
|
|||||||
|
<section class="menu-content-section" [attr.aria-labelledby]="titleId()">
|
||||||
|
<header class="menu-content-section__header">
|
||||||
|
<div class="menu-content-section__prefix">
|
||||||
|
<ng-content select="[menuContentPrefix]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="menu-content-section__title" [id]="titleId()">
|
||||||
|
{{ title() | uppercase }}
|
||||||
|
</h2>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="menu-content-section__body">
|
||||||
|
<ng-content />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-content-section {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-content-section__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-content-section__prefix:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-content-section__title {
|
||||||
|
margin: 0;
|
||||||
|
color: #888888;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-content-section__body {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.menu-content-section {
|
||||||
|
margin-inline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-content-section__header {
|
||||||
|
position: relative;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-content-section__prefix:not(:empty) {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-content-section__title {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { Component, signal } from '@angular/core';
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { Tenant } from '../../../../core/services/tenant.interface';
|
||||||
|
import { TenantService } from '../../../../core/services/tenant.service';
|
||||||
|
import { MenuContentSectionComponent } from './menu-content-section.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
imports: [MenuContentSectionComponent],
|
||||||
|
template: `
|
||||||
|
<app-menu-content-section menuCode="help.contact" titleId="contact-title">
|
||||||
|
<a menuContentPrefix href="/ayuda">Volver</a>
|
||||||
|
<p>Contenido proyectado</p>
|
||||||
|
</app-menu-content-section>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
class TestHostComponent {}
|
||||||
|
|
||||||
|
describe('MenuContentSectionComponent', () => {
|
||||||
|
it('renders an accessible title and projects its content', async () => {
|
||||||
|
const tenant = signal({
|
||||||
|
menues: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
code: 'help.contact',
|
||||||
|
label: 'Contacto del comercio',
|
||||||
|
parent_menu_code: 'help',
|
||||||
|
content_type: 'static',
|
||||||
|
route: '/ayuda/contacto',
|
||||||
|
submenues: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as unknown as Tenant);
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [TestHostComponent],
|
||||||
|
providers: [{ provide: TenantService, useValue: { tenant: tenant.asReadonly() } }],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(TestHostComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const element = fixture.nativeElement as HTMLElement;
|
||||||
|
const section = element.querySelector('section');
|
||||||
|
|
||||||
|
expect(section?.getAttribute('aria-labelledby')).toBe('contact-title');
|
||||||
|
expect(element.querySelector('#contact-title')?.textContent?.trim()).toBe(
|
||||||
|
'CONTACTO DEL COMERCIO',
|
||||||
|
);
|
||||||
|
expect(element.querySelector('.menu-content-section__prefix a')?.textContent).toContain(
|
||||||
|
'Volver',
|
||||||
|
);
|
||||||
|
expect(element.querySelector('.menu-content-section__body')?.textContent).toContain(
|
||||||
|
'Contenido proyectado',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { UpperCasePipe } from '@angular/common';
|
||||||
|
import { Component, computed, inject, input } from '@angular/core';
|
||||||
|
|
||||||
|
import { findMenu } from '../../../../core/services/menu.utils';
|
||||||
|
import { TenantService } from '../../../../core/services/tenant.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-menu-content-section',
|
||||||
|
imports: [UpperCasePipe],
|
||||||
|
templateUrl: './menu-content-section.component.html',
|
||||||
|
styleUrl: './menu-content-section.component.scss',
|
||||||
|
})
|
||||||
|
export class MenuContentSectionComponent {
|
||||||
|
private readonly tenantService = inject(TenantService);
|
||||||
|
|
||||||
|
readonly menuCode = input.required<string>();
|
||||||
|
readonly titleId = input.required<string>();
|
||||||
|
|
||||||
|
protected readonly title = computed(
|
||||||
|
() => findMenu(this.tenantService.tenant()?.menues ?? [], this.menuCode())?.label ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<div class="menu-page-layout">
|
||||||
|
<aside class="menu-page-layout__sidebar">
|
||||||
|
<h1 class="menu-page-layout__title">{{ menu()?.label }}</h1>
|
||||||
|
<div class="menu-page-layout__divider"></div>
|
||||||
|
|
||||||
|
<nav class="menu-page-layout__nav" [attr.aria-label]="navigationLabel">
|
||||||
|
@for (submenu of submenues(); track submenu.code) {
|
||||||
|
<a
|
||||||
|
class="menu-page-layout__link"
|
||||||
|
[routerLink]="submenu.route"
|
||||||
|
routerLinkActive="menu-page-layout__link--active"
|
||||||
|
>
|
||||||
|
{{ submenu.label }}
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="menu-page-layout__content">
|
||||||
|
<router-outlet />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-page-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(190px, 250px) minmax(0, 1fr);
|
||||||
|
gap: clamp(3rem, 8vw, 6.25rem);
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&__sidebar,
|
||||||
|
&__content {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__sidebar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
margin: 0 0 0.75rem;
|
||||||
|
color: #1a1a1a;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.25;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__divider {
|
||||||
|
height: 1px;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
background: #dedede;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__nav {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__link {
|
||||||
|
color: #888888;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.35;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 150ms ease;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus-visible {
|
||||||
|
color: #5a5a5a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
border-radius: 2px;
|
||||||
|
outline: 2px solid var(--color-primary);
|
||||||
|
outline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.menu-page-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 2rem;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__nav {
|
||||||
|
flex-flow: row wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.75rem 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import { signal } from '@angular/core';
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { ActivatedRoute, 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 { MenuPageLayoutComponent } from './menu-page-layout.component';
|
||||||
|
|
||||||
|
function createTenant(): Tenant {
|
||||||
|
return {
|
||||||
|
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: '',
|
||||||
|
categories: [],
|
||||||
|
menues: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
code: 'account',
|
||||||
|
label: 'Mi cuenta',
|
||||||
|
parent_menu_code: null,
|
||||||
|
content_type: 'dynamic',
|
||||||
|
route: '/mi-cuenta',
|
||||||
|
submenues: [
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
code: 'account.profile',
|
||||||
|
label: 'Datos personales',
|
||||||
|
parent_menu_code: 'account',
|
||||||
|
content_type: 'dynamic',
|
||||||
|
route: '/mi-cuenta/datos-personales',
|
||||||
|
submenues: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
code: 'account.purchases',
|
||||||
|
label: 'Mis compras',
|
||||||
|
parent_menu_code: 'account',
|
||||||
|
content_type: 'dynamic',
|
||||||
|
route: '/mi-cuenta/compras',
|
||||||
|
submenues: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
code: 'help',
|
||||||
|
label: 'Centro de ayuda',
|
||||||
|
parent_menu_code: null,
|
||||||
|
content_type: 'dynamic',
|
||||||
|
route: '/ayuda',
|
||||||
|
submenues: [
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
code: 'help.faq',
|
||||||
|
label: 'Consultas habituales',
|
||||||
|
parent_menu_code: 'help',
|
||||||
|
content_type: 'dynamic',
|
||||||
|
route: '/ayuda/preguntas-frecuentes',
|
||||||
|
submenues: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderLayout(menuCode: string, navigationLabel: string): Promise<HTMLElement> {
|
||||||
|
const tenant = signal(createTenant());
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [MenuPageLayoutComponent],
|
||||||
|
providers: [
|
||||||
|
provideRouter([]),
|
||||||
|
{
|
||||||
|
provide: ActivatedRoute,
|
||||||
|
useValue: {
|
||||||
|
snapshot: {
|
||||||
|
data: { menuCode, navigationLabel },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: TenantService,
|
||||||
|
useValue: { tenant: tenant.asReadonly() },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(MenuPageLayoutComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
return fixture.nativeElement as HTMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('MenuPageLayoutComponent', () => {
|
||||||
|
it('renders the configured account menu and its navigation label', async () => {
|
||||||
|
const element = await renderLayout('account', 'Secciones de mi cuenta');
|
||||||
|
const links = Array.from(
|
||||||
|
element.querySelectorAll<HTMLAnchorElement>('.menu-page-layout__link'),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(element.querySelector('.menu-page-layout__title')?.textContent?.trim()).toBe(
|
||||||
|
'Mi cuenta',
|
||||||
|
);
|
||||||
|
expect(element.querySelector('nav')?.getAttribute('aria-label')).toBe('Secciones de mi cuenta');
|
||||||
|
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',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses the same component for the help menu', async () => {
|
||||||
|
const element = await renderLayout('help', 'Secciones de ayuda');
|
||||||
|
|
||||||
|
expect(element.querySelector('.menu-page-layout__title')?.textContent?.trim()).toBe(
|
||||||
|
'Centro de ayuda',
|
||||||
|
);
|
||||||
|
expect(element.querySelector('.menu-page-layout__link')?.textContent?.trim()).toBe(
|
||||||
|
'Consultas habituales',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Component, computed, inject } from '@angular/core';
|
||||||
|
import { ActivatedRoute, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
|
||||||
|
|
||||||
|
import { findMenu } from '../../../../core/services/menu.utils';
|
||||||
|
import { TenantService } from '../../../../core/services/tenant.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-menu-page-layout',
|
||||||
|
imports: [RouterLink, RouterLinkActive, RouterOutlet],
|
||||||
|
templateUrl: './menu-page-layout.component.html',
|
||||||
|
styleUrl: './menu-page-layout.component.scss',
|
||||||
|
})
|
||||||
|
export class MenuPageLayoutComponent {
|
||||||
|
private readonly route = inject(ActivatedRoute);
|
||||||
|
private readonly tenantService = inject(TenantService);
|
||||||
|
|
||||||
|
protected readonly navigationLabel =
|
||||||
|
(this.route.snapshot.data['navigationLabel'] as string | undefined) ?? 'Secciones';
|
||||||
|
|
||||||
|
private readonly menuCode = this.route.snapshot.data['menuCode'] as string | undefined;
|
||||||
|
|
||||||
|
protected readonly menu = computed(() =>
|
||||||
|
findMenu(this.tenantService.tenant()?.menues ?? [], this.menuCode ?? ''),
|
||||||
|
);
|
||||||
|
protected readonly submenues = computed(() => this.menu()?.submenues ?? []);
|
||||||
|
}
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<div class="sidebar-container">
|
|
||||||
<h2 class="sidebar-title">{{ menu()?.label }}</h2>
|
|
||||||
<div class="divider"></div>
|
|
||||||
<nav class="sidebar-nav" aria-label="Secciones de mi cuenta">
|
|
||||||
@for (menu of submenues(); track menu.code) {
|
|
||||||
<a [routerLink]="menu.route" routerLinkActive="active" class="nav-link">
|
|
||||||
{{ menu.label }}
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
.sidebar-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
.sidebar-title {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #1a1a1a;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
.divider {
|
|
||||||
height: 1px;
|
|
||||||
background-color: #e0e0e0;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
.sidebar-nav {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
.nav-link {
|
|
||||||
text-decoration: none;
|
|
||||||
color: #888;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: color 0.2s;
|
|
||||||
}
|
|
||||||
.nav-link:hover {
|
|
||||||
color: #5a5a5a;
|
|
||||||
}
|
|
||||||
.nav-link.active {
|
|
||||||
color: var(--color-primary); /* Blue color matching the image */
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
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: '',
|
|
||||||
categories: [],
|
|
||||||
menues: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
code: 'account',
|
|
||||||
label: 'Mi cuenta',
|
|
||||||
parent_menu_code: null,
|
|
||||||
content_type: 'dynamic',
|
|
||||||
route: '/mi-cuenta',
|
|
||||||
submenues: [
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
code: 'account.profile',
|
|
||||||
label: 'Datos personales',
|
|
||||||
parent_menu_code: 'account',
|
|
||||||
content_type: 'dynamic',
|
|
||||||
route: '/mi-cuenta/datos-personales',
|
|
||||||
submenues: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
code: 'account.purchases',
|
|
||||||
label: 'Mis compras',
|
|
||||||
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(element.querySelector('.sidebar-title')?.textContent?.trim()).toBe('Mi cuenta');
|
|
||||||
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,21 +0,0 @@
|
|||||||
import { Component, computed, inject } from '@angular/core';
|
|
||||||
import { RouterLink, RouterLinkActive } from '@angular/router';
|
|
||||||
|
|
||||||
import { findMenu } from '../../../../../../core/services/menu.utils';
|
|
||||||
import { TenantService } from '../../../../../../core/services/tenant.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-account-sidebar',
|
|
||||||
standalone: true,
|
|
||||||
imports: [RouterLink, RouterLinkActive],
|
|
||||||
templateUrl: './account-sidebar.html',
|
|
||||||
styleUrl: './account-sidebar.scss',
|
|
||||||
})
|
|
||||||
export class AccountSidebar {
|
|
||||||
private readonly tenantService = inject(TenantService);
|
|
||||||
|
|
||||||
protected readonly menu = computed(() =>
|
|
||||||
findMenu(this.tenantService.tenant()?.menues ?? [], 'account'),
|
|
||||||
);
|
|
||||||
protected readonly submenues = computed(() => this.menu()?.submenues ?? []);
|
|
||||||
}
|
|
||||||
@@ -13,3 +13,13 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.form-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
<div class="page-container">
|
<app-menu-content-section
|
||||||
<h2 class="page-title">DATOS PERSONALES</h2>
|
class="account-page"
|
||||||
<app-profile-form></app-profile-form>
|
menuCode="account.profile"
|
||||||
</div>
|
titleId="profile-page-title"
|
||||||
|
>
|
||||||
|
<app-profile-form />
|
||||||
|
</app-menu-content-section>
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
.page-container {
|
:host {
|
||||||
display: flex;
|
display: block;
|
||||||
flex-direction: column;
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-page {
|
||||||
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-title {
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #A0A0A0;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
import { MenuContentSectionComponent } from '../../../../components/menu-content-section/menu-content-section.component';
|
||||||
import { ProfileForm } from '../../components/profile-form/profile-form';
|
import { ProfileForm } from '../../components/profile-form/profile-form';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-profile-page',
|
selector: 'app-profile-page',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [ProfileForm],
|
imports: [ProfileForm, MenuContentSectionComponent],
|
||||||
templateUrl: './profile-page.html',
|
templateUrl: './profile-page.html',
|
||||||
styleUrl: './profile-page.scss',
|
styleUrl: './profile-page.scss',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,26 +1,35 @@
|
|||||||
<div class="page-container">
|
<app-menu-content-section
|
||||||
<div class="d-flex align-items-center mb-4">
|
class="account-page"
|
||||||
<a routerLink="../" class="text-decoration-none " >
|
menuCode="account.purchases"
|
||||||
<i class="bi bi-arrow-left fs-5"></i>
|
titleId="purchase-detail-page-title"
|
||||||
</a>
|
>
|
||||||
<h2 class="page-title m-0">MIS COMPRAS</h2>
|
<a
|
||||||
</div>
|
menuContentPrefix
|
||||||
|
routerLink="../"
|
||||||
|
class="purchase-back-link text-decoration-none"
|
||||||
|
aria-label="Volver a mis compras"
|
||||||
|
>
|
||||||
|
<i class="bi bi-arrow-left fs-5" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
@if (isLoading()) {
|
@if (isLoading()) {
|
||||||
<p class="purchase-loading">Cargando detalle de compra...</p>
|
<p class="purchase-loading">Cargando detalle de compra...</p>
|
||||||
} @else if (purchase(); as purchase) {
|
} @else if (purchase(); as purchase) {
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4 pb-3" style="border-bottom: 1px solid #dddddd;">
|
<div
|
||||||
|
class="d-flex justify-content-between align-items-center mb-4 pb-3"
|
||||||
|
style="border-bottom: 1px solid #dddddd"
|
||||||
|
>
|
||||||
<div class="purchase-info">
|
<div class="purchase-info">
|
||||||
<span class="purchase-id">Compra {{ purchase.id }}.</span>
|
<span class="purchase-id">Compra {{ purchase.id }}.</span>
|
||||||
<span class="purchase-date">Fecha de compra: {{ purchase.date }}</span>
|
<span class="purchase-date">Fecha de compra: {{ purchase.date }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="purchase-total ">
|
<div class="purchase-total">
|
||||||
<span class="purchase-total-label">Total:</span>
|
<span class="purchase-total-label">Total:</span>
|
||||||
<span class="purchase-total-value">${{ purchase.total }}</span>
|
<span class="purchase-total-value">${{ purchase.total }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p style="font-size: 12px; color: #666666; margin:0;">Productos:</p>
|
<p style="font-size: 12px; color: #666666; margin: 0">Productos:</p>
|
||||||
|
|
||||||
<div class="d-flex flex-column">
|
<div class="d-flex flex-column">
|
||||||
@for (item of purchase.items; track item.id) {
|
@for (item of purchase.items; track item.id) {
|
||||||
@@ -30,4 +39,4 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</app-menu-content-section>
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
.page-container {
|
:host {
|
||||||
display: flex;
|
display: block;
|
||||||
flex-direction: column;
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-page {
|
||||||
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-title {
|
.purchase-back-link {
|
||||||
font-size: 15px;
|
color: #888888;
|
||||||
font-weight: bold;
|
|
||||||
color: #A0A0A0;
|
&:hover,
|
||||||
|
&:focus-visible {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.purchase-info {
|
.purchase-info {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
} from '../../../../../../core/services/checkout.service';
|
} from '../../../../../../core/services/checkout.service';
|
||||||
import { TenantService } from '../../../../../../core/services/tenant.service';
|
import { TenantService } from '../../../../../../core/services/tenant.service';
|
||||||
import { ToastService } from '../../../../../../core/services/toast.service';
|
import { ToastService } from '../../../../../../core/services/toast.service';
|
||||||
|
import { MenuContentSectionComponent } from '../../../../components/menu-content-section/menu-content-section.component';
|
||||||
import { PurchaseItem, PurchaseItemViewModel } from '../../components/purchase-item/purchase-item';
|
import { PurchaseItem, PurchaseItemViewModel } from '../../components/purchase-item/purchase-item';
|
||||||
|
|
||||||
type PurchaseDetailViewModel = {
|
type PurchaseDetailViewModel = {
|
||||||
@@ -20,7 +21,7 @@ type PurchaseDetailViewModel = {
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-purchase-detail-page',
|
selector: 'app-purchase-detail-page',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, RouterLink, PurchaseItem],
|
imports: [CommonModule, RouterLink, PurchaseItem, MenuContentSectionComponent],
|
||||||
templateUrl: './purchase-detail-page.html',
|
templateUrl: './purchase-detail-page.html',
|
||||||
styleUrl: './purchase-detail-page.scss',
|
styleUrl: './purchase-detail-page.scss',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
<div class="page-container">
|
<app-menu-content-section
|
||||||
<h2 class="page-title">MIS COMPRAS</h2>
|
class="account-page"
|
||||||
<app-purchase-list></app-purchase-list>
|
menuCode="account.purchases"
|
||||||
</div>
|
titleId="purchases-page-title"
|
||||||
|
>
|
||||||
|
<app-purchase-list />
|
||||||
|
</app-menu-content-section>
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
.page-container {
|
:host {
|
||||||
display: flex;
|
display: block;
|
||||||
flex-direction: column;
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-page {
|
||||||
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-title {
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #A0A0A0;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
import { MenuContentSectionComponent } from '../../../../components/menu-content-section/menu-content-section.component';
|
||||||
import { PurchaseList } from '../../components/purchase-list/purchase-list';
|
import { PurchaseList } from '../../components/purchase-list/purchase-list';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-purchases-page',
|
selector: 'app-purchases-page',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [PurchaseList],
|
imports: [PurchaseList, MenuContentSectionComponent],
|
||||||
templateUrl: './purchases-page.html',
|
templateUrl: './purchases-page.html',
|
||||||
styleUrl: './purchases-page.scss',
|
styleUrl: './purchases-page.scss',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<section class="tickets-page">
|
<app-menu-content-section
|
||||||
<h1 class="tickets-page__title">MIS TICKETS</h1>
|
class="tickets-page"
|
||||||
|
menuCode="account.tickets"
|
||||||
|
titleId="tickets-page-title"
|
||||||
|
>
|
||||||
<div class="tickets-list" [attr.aria-busy]="isLoading() || isGeneratingPdf()">
|
<div class="tickets-list" [attr.aria-busy]="isLoading() || isGeneratingPdf()">
|
||||||
@if (isGeneratingPdf()) {
|
@if (isGeneratingPdf()) {
|
||||||
<div class="tickets-list__generating" role="status" aria-live="polite">
|
<div class="tickets-list__generating" role="status" aria-live="polite">
|
||||||
@@ -106,4 +108,4 @@
|
|||||||
<p class="tickets-list__empty">Aún no tenés tickets disponibles.</p>
|
<p class="tickets-list__empty">Aún no tenés tickets disponibles.</p>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</app-menu-content-section>
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
.tickets-page {
|
.tickets-page {
|
||||||
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 720px;
|
max-width: 720px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tickets-page__title {
|
|
||||||
margin: 0 0 20px;
|
|
||||||
color: #a0a0a0;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tickets-list {
|
.tickets-list {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@@ -206,10 +200,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 767.98px) {
|
@media (max-width: 767.98px) {
|
||||||
.tickets-page__title {
|
|
||||||
margin-top: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tickets-list__skeleton {
|
.tickets-list__skeleton {
|
||||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
|
import { signal } from '@angular/core';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
import { describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { TenantService } from '../../../../../../core/services/tenant.service';
|
||||||
import { ToastService } from '../../../../../../core/services/toast.service';
|
import { ToastService } from '../../../../../../core/services/toast.service';
|
||||||
import { ModalService } from '../../../../../../core/services/modal.service';
|
import { ModalService } from '../../../../../../core/services/modal.service';
|
||||||
import { TicketComponent } from './components/ticket/ticket.component';
|
import { TicketComponent } from './components/ticket/ticket.component';
|
||||||
import { TicketResponse, TicketService } from './ticket.service';
|
import { TicketResponse, TicketService } from './ticket.service';
|
||||||
import { TicketsPage } from './tickets-page';
|
import { TicketsPage } from './tickets-page';
|
||||||
|
|
||||||
function withCustomLoading<T extends object>(
|
function withCustomLoading<T extends object>(service: T): T & { withCustomLoading: () => T } {
|
||||||
service: T,
|
|
||||||
): T & { withCustomLoading: () => T } {
|
|
||||||
return Object.assign(service, {
|
return Object.assign(service, {
|
||||||
withCustomLoading: () => service,
|
withCustomLoading: () => service,
|
||||||
});
|
});
|
||||||
@@ -34,6 +34,31 @@ const ticket = (overrides: Partial<TicketResponse>): TicketResponse => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('TicketsPage', () => {
|
describe('TicketsPage', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: TenantService,
|
||||||
|
useValue: {
|
||||||
|
tenant: signal({
|
||||||
|
menues: [
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
code: 'account.tickets',
|
||||||
|
label: 'Entradas disponibles',
|
||||||
|
parent_menu_code: 'account',
|
||||||
|
content_type: 'dynamic',
|
||||||
|
route: '/mi-cuenta/tickets',
|
||||||
|
submenues: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).asReadonly(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('shares the generated PDF as a file without text that Windows could prioritize', async () => {
|
it('shares the generated PDF as a file without text that Windows could prioritize', async () => {
|
||||||
const share = vi.fn().mockResolvedValue(undefined);
|
const share = vi.fn().mockResolvedValue(undefined);
|
||||||
const canShare = vi.fn().mockReturnValue(true);
|
const canShare = vi.fn().mockReturnValue(true);
|
||||||
@@ -92,11 +117,11 @@ describe('TicketsPage', () => {
|
|||||||
const createObjectUrl = vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:ticket');
|
const createObjectUrl = vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:ticket');
|
||||||
const revokeObjectUrl = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => undefined);
|
const revokeObjectUrl = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => undefined);
|
||||||
let downloadedFilename = '';
|
let downloadedFilename = '';
|
||||||
const linkClick = vi
|
const linkClick = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(function (
|
||||||
.spyOn(HTMLAnchorElement.prototype, 'click')
|
this: HTMLAnchorElement,
|
||||||
.mockImplementation(function (this: HTMLAnchorElement) {
|
) {
|
||||||
downloadedFilename = this.download;
|
downloadedFilename = this.download;
|
||||||
});
|
});
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [TicketsPage],
|
imports: [TicketsPage],
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ import { Component, computed, inject, OnInit, signal } from '@angular/core';
|
|||||||
import { ModalService } from '../../../../../../core/services/modal.service';
|
import { ModalService } from '../../../../../../core/services/modal.service';
|
||||||
import { ToastService } from '../../../../../../core/services/toast.service';
|
import { ToastService } from '../../../../../../core/services/toast.service';
|
||||||
import { IconButtonComponent } from '../../../../../../shared/components/icon-button/icon-button.component';
|
import { IconButtonComponent } from '../../../../../../shared/components/icon-button/icon-button.component';
|
||||||
|
import { MenuContentSectionComponent } from '../../../../components/menu-content-section/menu-content-section.component';
|
||||||
import { TicketComponent } from './components/ticket/ticket.component';
|
import { TicketComponent } from './components/ticket/ticket.component';
|
||||||
import { TicketResponse, TicketService } from './ticket.service';
|
import { TicketResponse, TicketService } from './ticket.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-tickets-page',
|
selector: 'app-tickets-page',
|
||||||
imports: [IconButtonComponent, TicketComponent],
|
imports: [IconButtonComponent, TicketComponent, MenuContentSectionComponent],
|
||||||
providers: [TicketService],
|
providers: [TicketService],
|
||||||
templateUrl: './tickets-page.html',
|
templateUrl: './tickets-page.html',
|
||||||
styleUrl: './tickets-page.scss',
|
styleUrl: './tickets-page.scss',
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
<div class="sidebar-layout">
|
|
||||||
<app-account-sidebar class="account-sidebar"></app-account-sidebar>
|
|
||||||
<div class="account-content">
|
|
||||||
<router-outlet></router-outlet>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
.sidebar-layout {
|
|
||||||
display: flex;
|
|
||||||
margin: 0 auto;
|
|
||||||
gap: 100px;
|
|
||||||
min-height: calc(100vh - 100px);
|
|
||||||
}
|
|
||||||
.account-sidebar {
|
|
||||||
width: 250px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.account-content {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
font-size: 13px !important;
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
import { RouterOutlet } from '@angular/router';
|
|
||||||
import { AccountSidebar } from '../components/account-sidebar/account-sidebar';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-sidebar-layout',
|
|
||||||
standalone: true,
|
|
||||||
imports: [RouterOutlet, AccountSidebar],
|
|
||||||
templateUrl: './sidebar-layout.html',
|
|
||||||
styleUrl: './sidebar-layout.scss',
|
|
||||||
})
|
|
||||||
export class SidebarLayout {}
|
|
||||||
@@ -20,6 +20,6 @@
|
|||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex: 1 1 290px;
|
flex: 1 1 290px;
|
||||||
max-width: 290px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,15 @@
|
|||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|
||||||
|
.checkout-page__cart-col {
|
||||||
|
order: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkout-page__stepper-col,
|
||||||
|
.checkout-page__editing-notice {
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__stepper-col {
|
&__stepper-col {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="payment-panel d-flex justify-content-end" aria-live="polite">
|
<section class="payment-panel" aria-live="polite">
|
||||||
@if (isGeneratingIntent()) {
|
@if (isGeneratingIntent()) {
|
||||||
<div class="payment-panel__card" style="text-align: center; padding: 2rem">
|
<div class="payment-panel__card" style="text-align: center; padding: 2rem">
|
||||||
<h3 class="payment-panel__title">Cargando información de pago...</h3>
|
<h3 class="payment-panel__title">Cargando información de pago...</h3>
|
||||||
|
|||||||
@@ -32,6 +32,11 @@
|
|||||||
.payment-methods {
|
.payment-methods {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
width: min(100%, 300px);
|
||||||
|
justify-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
&__title {
|
&__title {
|
||||||
margin: 0 0 1.75rem;
|
margin: 0 0 1.75rem;
|
||||||
color: #8a8a8a;
|
color: #8a8a8a;
|
||||||
@@ -109,7 +114,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.payment-panel {
|
.payment-panel {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
|
> app-checkout-payment-qr,
|
||||||
|
> app-checkout-payment-transfer,
|
||||||
|
> app-checkout-payment-telepagos {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
&__card {
|
&__card {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 320px;
|
max-width: 320px;
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
<aside class="help-sidebar">
|
|
||||||
<h1 class="help-sidebar__title">{{ menu()?.label }}</h1>
|
|
||||||
<div class="help-sidebar__divider"></div>
|
|
||||||
|
|
||||||
<nav class="help-sidebar__nav" aria-label="Secciones de ayuda">
|
|
||||||
@for (menu of submenues(); track menu.code) {
|
|
||||||
<a
|
|
||||||
class="help-sidebar__link"
|
|
||||||
[routerLink]="menu.route"
|
|
||||||
routerLinkActive="help-sidebar__link--active"
|
|
||||||
>
|
|
||||||
{{ menu.label }}
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
</nav>
|
|
||||||
</aside>
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
.help-sidebar {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.help-sidebar__title {
|
|
||||||
margin: 0 0 0.75rem;
|
|
||||||
color: #1a1a1a;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1.25;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.help-sidebar__divider {
|
|
||||||
height: 1px;
|
|
||||||
margin-bottom: 1.25rem;
|
|
||||||
background: #dedede;
|
|
||||||
}
|
|
||||||
|
|
||||||
.help-sidebar__nav {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.help-sidebar__link {
|
|
||||||
color: #888888;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 1.35;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: color 150ms ease;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus-visible {
|
|
||||||
color: #5a5a5a;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus-visible {
|
|
||||||
border-radius: 2px;
|
|
||||||
outline: 2px solid var(--color-primary);
|
|
||||||
outline-offset: 3px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.help-sidebar__link--active {
|
|
||||||
color: var(--color-primary);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 767.98px) {
|
|
||||||
.help-sidebar__nav {
|
|
||||||
flex-flow: row wrap;
|
|
||||||
gap: 0.75rem 1.25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
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 { HelpSidebar } from './help-sidebar';
|
|
||||||
|
|
||||||
describe('HelpSidebar', () => {
|
|
||||||
it('renders the parent and submenu labels supplied by 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: '',
|
|
||||||
categories: [],
|
|
||||||
menues: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
code: 'help',
|
|
||||||
label: 'Centro de ayuda',
|
|
||||||
parent_menu_code: null,
|
|
||||||
content_type: 'dynamic',
|
|
||||||
route: '/ayuda',
|
|
||||||
submenues: [
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
code: 'help.faq',
|
|
||||||
label: 'Consultas habituales',
|
|
||||||
parent_menu_code: 'help',
|
|
||||||
content_type: 'dynamic',
|
|
||||||
route: '/ayuda/preguntas-frecuentes',
|
|
||||||
submenues: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
imports: [HelpSidebar],
|
|
||||||
providers: [
|
|
||||||
provideRouter([]),
|
|
||||||
{
|
|
||||||
provide: TenantService,
|
|
||||||
useValue: { tenant: tenant.asReadonly() },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}).compileComponents();
|
|
||||||
|
|
||||||
const fixture = TestBed.createComponent(HelpSidebar);
|
|
||||||
fixture.detectChanges();
|
|
||||||
|
|
||||||
const element = fixture.nativeElement as HTMLElement;
|
|
||||||
|
|
||||||
expect(element.querySelector('.help-sidebar__title')?.textContent?.trim()).toBe(
|
|
||||||
'Centro de ayuda',
|
|
||||||
);
|
|
||||||
expect(element.querySelector('.help-sidebar__link')?.textContent?.trim()).toBe(
|
|
||||||
'Consultas habituales',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { Component, computed, inject } from '@angular/core';
|
|
||||||
import { RouterLink, RouterLinkActive } from '@angular/router';
|
|
||||||
|
|
||||||
import { findMenu } from '../../../../../../core/services/menu.utils';
|
|
||||||
import { TenantService } from '../../../../../../core/services/tenant.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-help-sidebar',
|
|
||||||
imports: [RouterLink, RouterLinkActive],
|
|
||||||
templateUrl: './help-sidebar.html',
|
|
||||||
styleUrl: './help-sidebar.scss',
|
|
||||||
})
|
|
||||||
export class HelpSidebar {
|
|
||||||
private readonly tenantService = inject(TenantService);
|
|
||||||
|
|
||||||
protected readonly menu = computed(() =>
|
|
||||||
findMenu(this.tenantService.tenant()?.menues ?? [], 'help'),
|
|
||||||
);
|
|
||||||
protected readonly submenues = computed(() => this.menu()?.submenues ?? []);
|
|
||||||
}
|
|
||||||
@@ -1,53 +1,53 @@
|
|||||||
<section class="contact-page" aria-labelledby="contact-page-title">
|
<app-menu-content-section menuCode="help.contact" titleId="contact-page-title">
|
||||||
<div class="contact-page__details">
|
<div class="contact-page">
|
||||||
<h2 id="contact-page-title" class="contact-page__title">CONTACTO</h2>
|
<div class="contact-page__details">
|
||||||
|
@if (contact().whatsappUrl && contact().whatsappLabel) {
|
||||||
|
<a
|
||||||
|
class="contact-page__contact-link"
|
||||||
|
[href]="contact().whatsappUrl"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<i class="fa-brands fa-whatsapp" aria-hidden="true"></i>
|
||||||
|
<span>{{ contact().whatsappLabel }}</span>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
|
||||||
@if (contact().whatsappUrl && contact().whatsappLabel) {
|
@if (contact().phone; as phone) {
|
||||||
<a
|
<a class="contact-page__contact-link" [href]="phoneUrl(phone)">
|
||||||
class="contact-page__contact-link"
|
<i class="fa-solid fa-phone" aria-hidden="true"></i>
|
||||||
[href]="contact().whatsappUrl"
|
<span>{{ phone }}</span>
|
||||||
target="_blank"
|
</a>
|
||||||
rel="noopener noreferrer"
|
}
|
||||||
>
|
|
||||||
<i class="fa-brands fa-whatsapp" aria-hidden="true"></i>
|
|
||||||
<span>{{ contact().whatsappLabel }}</span>
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (contact().phone; as phone) {
|
@for (location of contact().locations; track location.id) {
|
||||||
<a class="contact-page__contact-link" [href]="phoneUrl(phone)">
|
<article class="contact-page__location">
|
||||||
<i class="fa-solid fa-phone" aria-hidden="true"></i>
|
<h3 class="contact-page__location-title">
|
||||||
<span>{{ phone }}</span>
|
<i class="fa-solid fa-location-dot" aria-hidden="true"></i>
|
||||||
</a>
|
<span>{{ location.label }}</span>
|
||||||
}
|
</h3>
|
||||||
|
|
||||||
@for (location of contact().locations; track location.id) {
|
<ul class="contact-page__addresses">
|
||||||
<article class="contact-page__location">
|
@for (address of location.addresses; track address.id) {
|
||||||
<h3 class="contact-page__location-title">
|
<li>{{ address.address }}</li>
|
||||||
<i class="fa-solid fa-location-dot" aria-hidden="true"></i>
|
}
|
||||||
<span>{{ location.label }}</span>
|
</ul>
|
||||||
</h3>
|
</article>
|
||||||
|
}
|
||||||
|
|
||||||
<ul class="contact-page__addresses">
|
@if (!contact().whatsappUrl && !contact().phone && contact().locations.length === 0) {
|
||||||
@for (address of location.addresses; track address.id) {
|
<p class="contact-page__empty">No hay datos de contacto disponibles.</p>
|
||||||
<li>{{ address.address }}</li>
|
}
|
||||||
}
|
</div>
|
||||||
</ul>
|
|
||||||
</article>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (!contact().whatsappUrl && !contact().phone && contact().locations.length === 0) {
|
@if (contact().mapLocations.length > 0) {
|
||||||
<p class="contact-page__empty">No hay datos de contacto disponibles.</p>
|
<app-location-map
|
||||||
|
class="contact-page__map"
|
||||||
|
[locations]="contact().mapLocations"
|
||||||
|
[markerColor]="markerColor()"
|
||||||
|
mapStyle="minimal-light"
|
||||||
|
ariaLabel="Mapa de sucursales y puntos de atención"
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
</app-menu-content-section>
|
||||||
@if (contact().mapLocations.length > 0) {
|
|
||||||
<app-location-map
|
|
||||||
class="contact-page__map"
|
|
||||||
[locations]="contact().mapLocations"
|
|
||||||
[markerColor]="markerColor()"
|
|
||||||
mapStyle="minimal-light"
|
|
||||||
ariaLabel="Mapa de sucursales y puntos de atención"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</section>
|
|
||||||
|
|||||||
@@ -20,14 +20,6 @@
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contact-page__title {
|
|
||||||
margin: 0 0 1.25rem;
|
|
||||||
color: inherit;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-page__contact-link {
|
.contact-page__contact-link {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -122,6 +114,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.contact-page__details {
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-page__location {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-page__location-title {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-page__addresses {
|
||||||
|
padding-left: 0;
|
||||||
|
list-style-position: inside;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 575.98px) {
|
@media (max-width: 575.98px) {
|
||||||
.contact-page__map::ng-deep .location-map {
|
.contact-page__map::ng-deep .location-map {
|
||||||
min-height: 16rem;
|
min-height: 16rem;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Component, computed, inject } from '@angular/core';
|
|||||||
|
|
||||||
import { findMenu } from '../../../../../../core/services/menu.utils';
|
import { findMenu } from '../../../../../../core/services/menu.utils';
|
||||||
import { TenantService } from '../../../../../../core/services/tenant.service';
|
import { TenantService } from '../../../../../../core/services/tenant.service';
|
||||||
|
import { MenuContentSectionComponent } from '../../../../components/menu-content-section/menu-content-section.component';
|
||||||
import {
|
import {
|
||||||
LocationMapComponent,
|
LocationMapComponent,
|
||||||
MapLocation,
|
MapLocation,
|
||||||
@@ -30,7 +31,7 @@ type ContactContent = {
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-contact-page',
|
selector: 'app-contact-page',
|
||||||
imports: [LocationMapComponent],
|
imports: [LocationMapComponent, MenuContentSectionComponent],
|
||||||
templateUrl: './contact-page.html',
|
templateUrl: './contact-page.html',
|
||||||
styleUrl: './contact-page.scss',
|
styleUrl: './contact-page.scss',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,14 +1,8 @@
|
|||||||
<section class="faq-page" aria-labelledby="faq-page-title">
|
<app-menu-content-section class="faq-page" menuCode="help.faq" titleId="faq-page-title">
|
||||||
<h2 id="faq-page-title" class="faq-page__title">PREGUNTAS FRECUENTES</h2>
|
|
||||||
|
|
||||||
@if (faqs().length > 0) {
|
@if (faqs().length > 0) {
|
||||||
<app-accordion [flush]="true">
|
<app-accordion [flush]="true">
|
||||||
@for (faq of faqs(); track $index; let last = $last) {
|
@for (faq of faqs(); track $index; let last = $last) {
|
||||||
<app-accordion-item
|
<app-accordion-item [title]="faq.question" [headingLevel]="3" [open]="last">
|
||||||
[title]="faq.question"
|
|
||||||
[headingLevel]="3"
|
|
||||||
[open]="last"
|
|
||||||
>
|
|
||||||
<p>{{ faq.answer }}</p>
|
<p>{{ faq.answer }}</p>
|
||||||
</app-accordion-item>
|
</app-accordion-item>
|
||||||
}
|
}
|
||||||
@@ -16,4 +10,4 @@
|
|||||||
} @else {
|
} @else {
|
||||||
<p class="faq-page__empty">No hay preguntas frecuentes disponibles.</p>
|
<p class="faq-page__empty">No hay preguntas frecuentes disponibles.</p>
|
||||||
}
|
}
|
||||||
</section>
|
</app-menu-content-section>
|
||||||
|
|||||||
@@ -4,18 +4,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.faq-page {
|
.faq-page {
|
||||||
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 760px;
|
max-width: 760px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.faq-page__title {
|
|
||||||
margin: 0 0 0.5rem;
|
|
||||||
color: #a0a0a0;
|
|
||||||
font-size: 0.9375rem;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.faq-page p {
|
.faq-page p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { findMenu } from '../../../../../../core/services/menu.utils';
|
|||||||
import { TenantService } from '../../../../../../core/services/tenant.service';
|
import { TenantService } from '../../../../../../core/services/tenant.service';
|
||||||
import { AccordionItemComponent } from '../../../../../../shared/components/accordion/accordion-item.component';
|
import { AccordionItemComponent } from '../../../../../../shared/components/accordion/accordion-item.component';
|
||||||
import { AccordionComponent } from '../../../../../../shared/components/accordion/accordion.component';
|
import { AccordionComponent } from '../../../../../../shared/components/accordion/accordion.component';
|
||||||
|
import { MenuContentSectionComponent } from '../../../../components/menu-content-section/menu-content-section.component';
|
||||||
|
|
||||||
type Faq = {
|
type Faq = {
|
||||||
question: string;
|
question: string;
|
||||||
@@ -12,7 +13,7 @@ type Faq = {
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-faq-page',
|
selector: 'app-faq-page',
|
||||||
imports: [AccordionComponent, AccordionItemComponent],
|
imports: [AccordionComponent, AccordionItemComponent, MenuContentSectionComponent],
|
||||||
templateUrl: './faq-page.html',
|
templateUrl: './faq-page.html',
|
||||||
styleUrl: './faq-page.scss',
|
styleUrl: './faq-page.scss',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
<div class="help-layout">
|
|
||||||
<app-help-sidebar class="help-layout__sidebar" />
|
|
||||||
|
|
||||||
<div class="help-layout__content">
|
|
||||||
<router-outlet />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
:host {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.help-layout {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(190px, 250px) minmax(0, 1fr);
|
|
||||||
gap: clamp(3rem, 8vw, 6.25rem);
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.help-layout__sidebar {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.help-layout__content {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 767.98px) {
|
|
||||||
.help-layout {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
import { RouterOutlet } from '@angular/router';
|
|
||||||
|
|
||||||
import { HelpSidebar } from '../components/help-sidebar/help-sidebar';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-help-sidebar-layout',
|
|
||||||
imports: [RouterOutlet, HelpSidebar],
|
|
||||||
templateUrl: './help-sidebar-layout.html',
|
|
||||||
styleUrl: './help-sidebar-layout.scss',
|
|
||||||
})
|
|
||||||
export class HelpSidebarLayout {}
|
|
||||||
@@ -116,9 +116,13 @@ export const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'ayuda',
|
path: 'ayuda',
|
||||||
canActivate: [hasMenuGuard('help')],
|
canActivate: [hasMenuGuard('help')],
|
||||||
|
data: {
|
||||||
|
menuCode: 'help',
|
||||||
|
navigationLabel: 'Secciones de ayuda',
|
||||||
|
},
|
||||||
loadComponent: () =>
|
loadComponent: () =>
|
||||||
import('./pages/help-page/sidebar-layout/help-sidebar-layout').then(
|
import('./components/menu-page-layout/menu-page-layout.component').then(
|
||||||
(m) => m.HelpSidebarLayout,
|
(m) => m.MenuPageLayoutComponent,
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
@@ -152,8 +156,14 @@ export const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'mi-cuenta',
|
path: 'mi-cuenta',
|
||||||
canActivate: [authGuard, hasMenuGuard('account')],
|
canActivate: [authGuard, hasMenuGuard('account')],
|
||||||
|
data: {
|
||||||
|
menuCode: 'account',
|
||||||
|
navigationLabel: 'Secciones de mi cuenta',
|
||||||
|
},
|
||||||
loadComponent: () =>
|
loadComponent: () =>
|
||||||
import('./pages/account-page/sidebar-layout/sidebar-layout').then((m) => m.SidebarLayout),
|
import('./components/menu-page-layout/menu-page-layout.component').then(
|
||||||
|
(m) => m.MenuPageLayoutComponent,
|
||||||
|
),
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'datos-personales',
|
path: 'datos-personales',
|
||||||
|
|||||||
@@ -87,3 +87,13 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.cart-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
::ng-deep app-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -75,3 +75,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.product-column-with-image {
|
||||||
|
border-color: var(--color-primary, var(--tenant-primary, #009933));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user