feat: enhance product ticket selector and vertical cart card components

- Refactor ProductTicketSelectorComponent to improve variant selection logic and error handling.
- Introduce a new signal for variant attributes and manage variant loading more efficiently.
- Update the UI to reflect saving state in ProductVerticalWithCartCardComponent, preventing actions while saving.
- Adjust button text based on saving state for better user feedback.
This commit is contained in:
2026-08-18 10:45:30 -03:00
parent 69f3569ccf
commit 6b2f8de210
18 changed files with 644 additions and 617 deletions

View File

@@ -17,6 +17,7 @@
[layout]="categoryResults.layout"
[groupLayout]="paginatedLayout"
[items]="categoryResults"
[savingProductIds]="savingProductIds()"
(buy)="onBuyProduct($event)"
(addToCart)="onAddToCart($event)"
(pageChange)="onPageChange($event)"

View File

@@ -9,7 +9,16 @@ import {
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ActivatedRoute, Router } from '@angular/router';
import { catchError, combineLatest, distinctUntilChanged, map, of, switchMap, tap } from 'rxjs';
import {
catchError,
combineLatest,
distinctUntilChanged,
finalize,
map,
of,
switchMap,
tap,
} from 'rxjs';
import { AuthService } from '../../../../core/services/auth/auth.service';
import { CartService } from '../../../../core/services/cart/cart.service';
@@ -52,6 +61,7 @@ export class CategoryItemsPageComponent {
protected readonly results = signal<CategoryItemsResponse | null>(null);
protected readonly error = signal<string | null>(null);
protected readonly creatingDirectPurchase = signal(false);
protected readonly savingProductIds = signal<ReadonlySet<number>>(new Set<number>());
protected readonly paginatedLayout: CatalogGroupLayout = 'paginated';
constructor() {
@@ -157,14 +167,31 @@ export class CategoryItemsPageComponent {
}
protected onAddToCart(event: ProductListCartEvent): void {
this.cartService.addItem(event.product.id, event.variant ?? null, event.quantity).subscribe({
next: (response) => {
this.toastService.success(response.message || 'Producto agregado al carrito');
},
error: (error: HttpErrorResponse) => {
const message = error.error?.message || 'No se pudo agregar el producto al carrito.';
this.toastService.danger(message);
},
const productId = event.product.id;
if (this.savingProductIds().has(productId)) {
return;
}
this.setProductSaving(productId, true);
this.cartService
.addItem(productId, event.variant ?? null, event.quantity)
.pipe(finalize(() => this.setProductSaving(productId, false)))
.subscribe({
next: (response) => {
this.toastService.success(response.message || 'Producto agregado al carrito');
},
error: (error: HttpErrorResponse) => {
const message = error.error?.message || 'No se pudo agregar el producto al carrito.';
this.toastService.danger(message);
},
});
}
private setProductSaving(productId: number, saving: boolean): void {
this.savingProductIds.update((current) => {
const updated = new Set(current);
saving ? updated.add(productId) : updated.delete(productId);
return updated;
});
}

View File

@@ -63,7 +63,9 @@
[disabled]="!selectedVariantAvailable() || variantLoading() || addingToCart()"
(click)="addToCart()"
>
@if (variantLoading() || addingToCart()) {
@if (addingToCart()) {
Guardando
} @else if (variantLoading()) {
<div class="spinner-border spinner-border-sm" role="status"></div>
} @else {
Agregar al carrito

View File

@@ -20,6 +20,7 @@
[layout]="productLayout()"
[groupLayout]="groupLayout()"
[items]="displayItems()"
[savingProductIds]="savingProductIds()"
(buy)="onBuyProduct($event)"
(addToCart)="onAddToCart($event)"
(pageChange)="onPageChange($event)"

View File

@@ -9,7 +9,7 @@ import {
signal,
} from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { catchError, distinctUntilChanged, map, of, switchMap, tap } from 'rxjs';
import { catchError, distinctUntilChanged, finalize, map, of, switchMap, tap } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CartService } from '../../../../core/services/cart/cart.service';
@@ -58,6 +58,7 @@ export class SearchPageComponent {
protected readonly results = signal<ApiPaginatedResponse<CatalogFeaturedItem[]> | null>(null);
protected readonly error = signal<string | null>(null);
protected readonly creatingDirectPurchase = signal(false);
protected readonly savingProductIds = signal<ReadonlySet<number>>(new Set<number>());
protected readonly productLayout = computed<CatalogProductLayout>(
() => this.tenantService.tenant()?.search_product_layout ?? 'column_with_image',
@@ -189,14 +190,31 @@ export class SearchPageComponent {
}
protected onAddToCart(event: ProductListCartEvent): void {
this.cartService.addItem(event.product.id, event.variant ?? null, event.quantity).subscribe({
next: (response) => {
this.toastService.success(response.message || 'Producto agregado al carrito');
},
error: (error: HttpErrorResponse) => {
const message = error.error?.message || 'No se pudo agregar el producto al carrito.';
this.toastService.danger(message);
},
const productId = event.product.id;
if (this.savingProductIds().has(productId)) {
return;
}
this.setProductSaving(productId, true);
this.cartService
.addItem(productId, event.variant ?? null, event.quantity)
.pipe(finalize(() => this.setProductSaving(productId, false)))
.subscribe({
next: (response) => {
this.toastService.success(response.message || 'Producto agregado al carrito');
},
error: (error: HttpErrorResponse) => {
const message = error.error?.message || 'No se pudo agregar el producto al carrito.';
this.toastService.danger(message);
},
});
}
private setProductSaving(productId: number, saving: boolean): void {
this.savingProductIds.update((current) => {
const updated = new Set(current);
saving ? updated.add(productId) : updated.delete(productId);
return updated;
});
}

View File

@@ -31,6 +31,7 @@
[loading]="isGroupLoading(group.id)"
[loadImages]="!hasMainCarouselImages() || mainCarouselReady()"
[unavailableVariantIds]="unavailableVariantIds()"
[savingProductIds]="savingProductIds()"
(buy)="onBuyProduct($event)"
(addToCart)="onAddToCart($event)"
(pageChange)="onPageChange(group.id, $event)"

View File

@@ -10,7 +10,7 @@ import {
} from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { finalize, Subscription } from 'rxjs';
import { CartService } from '../../../../core/services/cart/cart.service';
import { AuthService } from '../../../../core/services/auth/auth.service';
@@ -64,6 +64,7 @@ export class StoreHomePageComponent implements OnInit, OnDestroy {
protected readonly mainCarouselReady = signal(false);
protected readonly creatingDirectPurchase = signal(false);
protected readonly unavailableVariantIds = signal<ReadonlySet<number>>(new Set<number>());
protected readonly savingProductIds = signal<ReadonlySet<number>>(new Set<number>());
protected readonly mainCarouselImages = computed(() => this.tenant()?.extras?.carousel ?? []);
protected readonly heroConfig = computed(() => this.tenant()?.extras?.heroConfig ?? null);
protected readonly additionalInfo = computed(
@@ -230,14 +231,31 @@ export class StoreHomePageComponent implements OnInit, OnDestroy {
}
protected onAddToCart(event: ProductListCartEvent): void {
this.cartService.addItem(event.product.id, event.variant ?? null, event.quantity).subscribe({
next: (response) => {
this.toastService.success(response.message || 'Producto agregado al carrito');
},
error: (error: HttpErrorResponse) => {
const message = error.error?.message || 'No se pudo agregar el producto al carrito.';
this.toastService.danger(message);
},
const productId = event.product.id;
if (this.savingProductIds().has(productId)) {
return;
}
this.setProductSaving(productId, true);
this.cartService
.addItem(productId, event.variant ?? null, event.quantity)
.pipe(finalize(() => this.setProductSaving(productId, false)))
.subscribe({
next: (response) => {
this.toastService.success(response.message || 'Producto agregado al carrito');
},
error: (error: HttpErrorResponse) => {
const message = error.error?.message || 'No se pudo agregar el producto al carrito.';
this.toastService.danger(message);
},
});
}
private setProductSaving(productId: number, saving: boolean): void {
this.savingProductIds.update((current) => {
const updated = new Set(current);
saving ? updated.add(productId) : updated.delete(productId);
return updated;
});
}