From b61ae3cb3299917d6e28abda1f28ca63cf52a410 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Wed, 1 Jul 2026 12:25:15 -0300 Subject: [PATCH] feat: implement catalog service and product attribute selector component for product details --- .../core/services/api-response.interface.ts | 2 + .../core/services/catalog/catalog.service.ts | 9 +++- .../product-attribute-selector.component.ts | 4 ++ .../product-detail-page.component.html | 16 +++++-- .../product-detail-page.component.ts | 47 +++++++++++++++++++ 5 files changed, 72 insertions(+), 6 deletions(-) diff --git a/src/app/core/services/api-response.interface.ts b/src/app/core/services/api-response.interface.ts index 3db5cbb..98fbf9e 100644 --- a/src/app/core/services/api-response.interface.ts +++ b/src/app/core/services/api-response.interface.ts @@ -1,3 +1,5 @@ export interface ApiResponse { data: T; + message?: string; + errors?: Record; } diff --git a/src/app/core/services/catalog/catalog.service.ts b/src/app/core/services/catalog/catalog.service.ts index 3a40197..926f4c2 100644 --- a/src/app/core/services/catalog/catalog.service.ts +++ b/src/app/core/services/catalog/catalog.service.ts @@ -35,9 +35,14 @@ export class CatalogService { ); } - getProducto(id: number): Observable { + getProducto(id: number, variantId?: number): Observable { + let params = new HttpParams(); + if (variantId) { + params = params.set('variant_id', variantId); + } + return this.http - .get>(`${this.tenantApiUrl}/productos/${id}`) + .get>(`${this.tenantApiUrl}/productos/${id}`, { params }) .pipe(map((response) => response.data)); } diff --git a/src/app/features/store/components/product-attribute-selector/product-attribute-selector.component.ts b/src/app/features/store/components/product-attribute-selector/product-attribute-selector.component.ts index cd84bba..23425c2 100644 --- a/src/app/features/store/components/product-attribute-selector/product-attribute-selector.component.ts +++ b/src/app/features/store/components/product-attribute-selector/product-attribute-selector.component.ts @@ -77,6 +77,10 @@ export class ProductAttributeSelectorComponent { return availability; }); + public reset(): void { + this.initializeSelections(this.attributes(), this.defaultVariant()); + } + constructor() { effect(() => { const attributes = this.attributes(); diff --git a/src/app/features/store/pages/product-detail-page/product-detail-page.component.html b/src/app/features/store/pages/product-detail-page/product-detail-page.component.html index e927df7..64e0457 100644 --- a/src/app/features/store/pages/product-detail-page/product-detail-page.component.html +++ b/src/app/features/store/pages/product-detail-page/product-detail-page.component.html @@ -81,13 +81,21 @@ class="product-detail__cta" variant="secondary" type="button" - [disabled]="!selectedVariant()" + [disabled]="!selectedVariant() || variantLoading()" > - Agregar al carrito + @if (variantLoading()) { +
+ } @else { + Agregar al carrito + } - - Comprar + + @if (variantLoading()) { +
+ } @else { + Comprar + }
diff --git a/src/app/features/store/pages/product-detail-page/product-detail-page.component.ts b/src/app/features/store/pages/product-detail-page/product-detail-page.component.ts index c0d91fc..a761ce7 100644 --- a/src/app/features/store/pages/product-detail-page/product-detail-page.component.ts +++ b/src/app/features/store/pages/product-detail-page/product-detail-page.component.ts @@ -12,10 +12,12 @@ import { PLATFORM_ID } from '@angular/core'; import { CommonModule, isPlatformBrowser } from '@angular/common'; +import { HttpErrorResponse } from '@angular/common/http'; import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { Subscription } from 'rxjs'; import { CatalogService } from '../../../../core/services/catalog/catalog.service'; +import { ToastService } from '../../../../core/services/toast.service'; import { ProductAttribute, ProductAttributeOption, @@ -39,6 +41,8 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy { private readonly route = inject(ActivatedRoute); private readonly router = inject(Router); private readonly catalogService = inject(CatalogService); + private readonly toastService = inject(ToastService); + private readonly attributeSelector = viewChild(ProductAttributeSelectorComponent); private readonly carouselHost = viewChild>('carouselHost'); private readonly descriptionBody = viewChild>('descriptionBody'); private readonly platformId = inject(PLATFORM_ID); @@ -63,6 +67,7 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy { return []; }); protected readonly loading = signal(false); + protected readonly variantLoading = signal(false); protected readonly error = signal(null); protected readonly selectedVariant = signal(null); protected readonly quantity = signal(1); @@ -136,6 +141,39 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy { }); } + private loadProductVariant(productId: number, variantId: number): void { + this.variantLoading.set(true); + + this.productSub?.unsubscribe(); + this.productSub = this.catalogService.getProducto(productId, variantId).subscribe({ + next: (prod) => { + this.product.set(prod); + this.variantLoading.set(false); + }, + error: (err: HttpErrorResponse) => { + const errorMessage = err.error?.message || 'No pudimos cargar la variante seleccionada.'; + this.toastService.danger(errorMessage); + this.variantLoading.set(false); + + this.product.update((currentProduct) => { + if (!currentProduct) return null; + const updatedVariantsMap = currentProduct.variants_map.map((v) => { + if (v.variant_id === variantId) { + return { ...v, stock: 0 }; + } + return v; + }); + return { + ...currentProduct, + variants_map: updatedVariantsMap + }; + }); + + this.attributeSelector()?.reset(); + } + }); + } + protected goBack(): void { this.router.navigate(['/']); } @@ -151,10 +189,19 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy { } protected onVariantChange(variant: ProductVariantMap | null): void { + if (this.selectedVariant()?.variant_id === variant?.variant_id) { + return; + } + this.selectedVariant.set(variant); if (variant && this.quantity() > variant.stock) { this.quantity.set(Math.max(1, variant.stock)); } + + const currentProduct = this.product(); + if (variant && currentProduct && currentProduct.variant?.id !== variant.variant_id) { + this.loadProductVariant(currentProduct.id, variant.variant_id); + } } protected increaseQuantity(): void {