diff --git a/src/app/core/layout/store-layout/store-header/store-header.component.html b/src/app/core/layout/store-layout/store-header/store-header.component.html index 61d06cd..2a385e4 100644 --- a/src/app/core/layout/store-layout/store-header/store-header.component.html +++ b/src/app/core/layout/store-layout/store-header/store-header.component.html @@ -153,7 +153,7 @@
-
+
@if (displayCategories()) {
} + + @if (checkoutRemainingTime(); as remainingTime) { +
+ Tiempo restante de compra: + {{ remainingTime }} +
+ }
diff --git a/src/app/core/layout/store-layout/store-header/store-header.component.scss b/src/app/core/layout/store-layout/store-header/store-header.component.scss index f86987c..256dd7c 100644 --- a/src/app/core/layout/store-layout/store-header/store-header.component.scss +++ b/src/app/core/layout/store-layout/store-header/store-header.component.scss @@ -14,6 +14,22 @@ height: 3.5rem; } +.store-layout__checkout-timer { + color: var(--tenant-primary); + font-weight: 700; + white-space: nowrap; +} + +.store-layout__checkout-timer-label { + font-size: 13px; +} + +.store-layout__checkout-timer-value { + font-size: 20px; + line-height: 1; + font-variant-numeric: tabular-nums; +} + .store-layout__brand-slot { min-width: 150px; } diff --git a/src/app/core/layout/store-layout/store-header/store-header.component.ts b/src/app/core/layout/store-layout/store-header/store-header.component.ts index 9d7723b..dfe1831 100644 --- a/src/app/core/layout/store-layout/store-header/store-header.component.ts +++ b/src/app/core/layout/store-layout/store-header/store-header.component.ts @@ -1,4 +1,13 @@ -import { Component, ElementRef, HostListener, inject, input, output, signal } from '@angular/core'; +import { + Component, + computed, + ElementRef, + HostListener, + inject, + input, + output, + signal, +} from '@angular/core'; import { Router, RouterLink } from '@angular/router'; import { AuthUser } from '../../../services/auth/auth.interfaces'; import { CartIconComponent } from '../../../../shared/components/cart-icon/cart-icon.component'; @@ -40,6 +49,7 @@ export class StoreHeaderComponent { readonly displaySeachBar = input(true); readonly displayCart = input(true); readonly cartDisabled = input(false); + readonly checkoutRemainingSeconds = input(null); readonly cartClick = output(); readonly ticketsClick = output(); readonly loginClick = output(); @@ -53,6 +63,18 @@ export class StoreHeaderComponent { protected readonly minSearchLength = 3; protected readonly showSearchError = signal(false); protected readonly searchControl = new FormControl('', { nonNullable: true }); + protected readonly checkoutRemainingTime = computed(() => { + const remainingSeconds = this.checkoutRemainingSeconds(); + + if (remainingSeconds === null) { + return null; + } + + const minutes = Math.floor(remainingSeconds / 60); + const seconds = remainingSeconds % 60; + + return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; + }); protected onCartClick(): void { if (this.cartDisabled()) { diff --git a/src/app/core/layout/store-layout/store-layout.component.html b/src/app/core/layout/store-layout/store-layout.component.html index 896168b..01f5dfe 100644 --- a/src/app/core/layout/store-layout/store-layout.component.html +++ b/src/app/core/layout/store-layout/store-layout.component.html @@ -13,6 +13,7 @@ [displaySeachBar]="tenant()?.display_seach_bar ?? true" [displayCart]="displayCart()" [cartDisabled]="isCheckoutRoute()" + [checkoutRemainingSeconds]="isCheckoutRoute() ? checkoutRemainingSeconds() : null" (cartClick)="onCartClick()" (ticketsClick)="onTicketsClick()" (loginClick)="onLoginClick()" diff --git a/src/app/core/layout/store-layout/store-layout.component.spec.ts b/src/app/core/layout/store-layout/store-layout.component.spec.ts index 2f6f160..b222a46 100644 --- a/src/app/core/layout/store-layout/store-layout.component.spec.ts +++ b/src/app/core/layout/store-layout/store-layout.component.spec.ts @@ -24,6 +24,7 @@ import { AuthUser } from '../../services/auth/auth.interfaces'; import { StoreLayoutComponent } from './store-layout.component'; import { StoreHeaderComponent } from './store-header/store-header.component'; import { CheckoutService } from '../../services/checkout.service'; +import { CheckoutCountdownService } from '../../services/checkout-countdown.service'; import { TenantUrlSerializer } from '../../services/tenant-url.serializer'; const tenant: Tenant = { @@ -150,6 +151,7 @@ describe('StoreLayoutComponent', () => { let tenantState = signal(tenant); let cartState = signal(null); let authUserState = signal(null); + let checkoutRemainingSecondsState = signal(null); let checkoutServiceStub: { startCheckout: ReturnType }; let queryParamMapState: BehaviorSubject; @@ -157,6 +159,7 @@ describe('StoreLayoutComponent', () => { tenantState = signal(tenant); cartState = signal(null); authUserState = signal(null); + checkoutRemainingSecondsState = signal(null); queryParamMapState = new BehaviorSubject(convertToParamMap({})); const isAuthenticatedState = signal(false); checkoutServiceStub = { @@ -215,6 +218,12 @@ describe('StoreLayoutComponent', () => { provide: CheckoutService, useValue: checkoutServiceStub, }, + { + provide: CheckoutCountdownService, + useValue: { + remainingSeconds: checkoutRemainingSecondsState.asReadonly(), + }, + }, ], }).compileComponents(); }); @@ -271,6 +280,24 @@ describe('StoreLayoutComponent', () => { expect(element.querySelector('.store-layout__cart-overlay')).toBeNull(); }); + it('shows the synchronized checkout countdown aligned in the header', () => { + const router = TestBed.inject(Router); + vi.spyOn(router, 'url', 'get').mockReturnValue('/checkout/25'); + checkoutRemainingSecondsState.set(587); + + const fixture = TestBed.createComponent(StoreLayoutComponent); + fixture.detectChanges(); + + const element = fixture.nativeElement as HTMLElement; + + expect(element.querySelector('.store-layout__checkout-timer-label')?.textContent).toContain( + 'Tiempo restante de compra:', + ); + expect(element.querySelector('.store-layout__checkout-timer-value')?.textContent?.trim()).toBe( + '09:47', + ); + }); + it('hides the configured header elements when the tenant disables them', () => { tenantState.set({ ...tenant, diff --git a/src/app/core/layout/store-layout/store-layout.component.ts b/src/app/core/layout/store-layout/store-layout.component.ts index e812afc..ccd0869 100644 --- a/src/app/core/layout/store-layout/store-layout.component.ts +++ b/src/app/core/layout/store-layout/store-layout.component.ts @@ -24,6 +24,7 @@ import { } from '../../services/checkout.service'; import { ToastService } from '../../services/toast.service'; import { Category } from '../../services/tenant.interface'; +import { CheckoutCountdownService } from '../../services/checkout-countdown.service'; @Component({ selector: 'app-store-layout', @@ -42,6 +43,7 @@ export class StoreLayoutComponent implements OnInit { private readonly cartService = inject(CartService); private readonly authService = inject(AuthService); private readonly checkoutService = inject(CheckoutService); + private readonly checkoutCountdownService = inject(CheckoutCountdownService); private readonly toastService = inject(ToastService); private readonly router = inject(Router); private readonly route = inject(ActivatedRoute); @@ -49,6 +51,7 @@ export class StoreLayoutComponent implements OnInit { protected readonly isCartOpen = signal(false); protected readonly isCheckoutRoute = signal(this.isCheckoutUrl(this.router.url)); + protected readonly checkoutRemainingSeconds = this.checkoutCountdownService.remainingSeconds; protected readonly isCreatingPurchase = signal(false); protected readonly displayCart = computed(() => this.tenant()?.display_cart ?? true); protected readonly cartEditingPolicy = computed(() => this.tenant()?.cart_editing_policy ?? null);