feat: implement catalog service and product attribute selector component for product details

This commit is contained in:
2026-07-01 12:25:15 -03:00
parent 3b7615fd85
commit b61ae3cb32
5 changed files with 72 additions and 6 deletions

View File

@@ -1,3 +1,5 @@
export interface ApiResponse<T> { export interface ApiResponse<T> {
data: T; data: T;
message?: string;
errors?: Record<string, string[]>;
} }

View File

@@ -35,9 +35,14 @@ export class CatalogService {
); );
} }
getProducto(id: number): Observable<ProductDetail> { getProducto(id: number, variantId?: number): Observable<ProductDetail> {
let params = new HttpParams();
if (variantId) {
params = params.set('variant_id', variantId);
}
return this.http return this.http
.get<ApiResponse<ProductDetail>>(`${this.tenantApiUrl}/productos/${id}`) .get<ApiResponse<ProductDetail>>(`${this.tenantApiUrl}/productos/${id}`, { params })
.pipe(map((response) => response.data)); .pipe(map((response) => response.data));
} }

View File

@@ -77,6 +77,10 @@ export class ProductAttributeSelectorComponent {
return availability; return availability;
}); });
public reset(): void {
this.initializeSelections(this.attributes(), this.defaultVariant());
}
constructor() { constructor() {
effect(() => { effect(() => {
const attributes = this.attributes(); const attributes = this.attributes();

View File

@@ -81,13 +81,21 @@
class="product-detail__cta" class="product-detail__cta"
variant="secondary" variant="secondary"
type="button" type="button"
[disabled]="!selectedVariant()" [disabled]="!selectedVariant() || variantLoading()"
> >
Agregar al carrito @if (variantLoading()) {
<div class="spinner-border spinner-border-sm" role="status"></div>
} @else {
Agregar al carrito
}
</app-button> </app-button>
<app-button class="product-detail__cta" type="button" [disabled]="!selectedVariant()"> <app-button class="product-detail__cta" type="button" [disabled]="!selectedVariant() || variantLoading()">
Comprar @if (variantLoading()) {
<div class="spinner-border spinner-border-sm" role="status"></div>
} @else {
Comprar
}
</app-button> </app-button>
</div> </div>
</div> </div>

View File

@@ -12,10 +12,12 @@ import {
PLATFORM_ID PLATFORM_ID
} from '@angular/core'; } from '@angular/core';
import { CommonModule, isPlatformBrowser } from '@angular/common'; import { CommonModule, isPlatformBrowser } from '@angular/common';
import { HttpErrorResponse } from '@angular/common/http';
import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { CatalogService } from '../../../../core/services/catalog/catalog.service'; import { CatalogService } from '../../../../core/services/catalog/catalog.service';
import { ToastService } from '../../../../core/services/toast.service';
import { import {
ProductAttribute, ProductAttribute,
ProductAttributeOption, ProductAttributeOption,
@@ -39,6 +41,8 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy {
private readonly route = inject(ActivatedRoute); private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router); private readonly router = inject(Router);
private readonly catalogService = inject(CatalogService); private readonly catalogService = inject(CatalogService);
private readonly toastService = inject(ToastService);
private readonly attributeSelector = viewChild(ProductAttributeSelectorComponent);
private readonly carouselHost = viewChild<ElementRef<HTMLElement>>('carouselHost'); private readonly carouselHost = viewChild<ElementRef<HTMLElement>>('carouselHost');
private readonly descriptionBody = viewChild<ElementRef<HTMLElement>>('descriptionBody'); private readonly descriptionBody = viewChild<ElementRef<HTMLElement>>('descriptionBody');
private readonly platformId = inject(PLATFORM_ID); private readonly platformId = inject(PLATFORM_ID);
@@ -63,6 +67,7 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy {
return []; return [];
}); });
protected readonly loading = signal(false); protected readonly loading = signal(false);
protected readonly variantLoading = signal(false);
protected readonly error = signal<string | null>(null); protected readonly error = signal<string | null>(null);
protected readonly selectedVariant = signal<ProductVariantMap | null>(null); protected readonly selectedVariant = signal<ProductVariantMap | null>(null);
protected readonly quantity = signal(1); 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 { protected goBack(): void {
this.router.navigate(['/']); this.router.navigate(['/']);
} }
@@ -151,10 +189,19 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy {
} }
protected onVariantChange(variant: ProductVariantMap | null): void { protected onVariantChange(variant: ProductVariantMap | null): void {
if (this.selectedVariant()?.variant_id === variant?.variant_id) {
return;
}
this.selectedVariant.set(variant); this.selectedVariant.set(variant);
if (variant && this.quantity() > variant.stock) { if (variant && this.quantity() > variant.stock) {
this.quantity.set(Math.max(1, 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 { protected increaseQuantity(): void {