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 5700e82..21e8c08 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 @@ -7,6 +7,7 @@ import { of } from 'rxjs'; import { Tenant } from '../../services/tenant.interface'; import { TenantService } from '../../services/tenant.service'; import { CartService } from '../../services/cart/cart.service'; +import { ToastService } from '../../services/toast.service'; import { StoreLayoutComponent } from './store-layout.component'; const tenant: Tenant = { @@ -45,7 +46,19 @@ describe('StoreLayoutComponent', () => { provide: CartService, useValue: { cart: signal(null).asReadonly(), - loadCart: () => of({ id: 1, items: [], subtotal: '0' }) + loadCart: () => of({ id: 1, items: [], subtotal: '0' }), + updateItemQuantity: vi.fn().mockReturnValue(of({ data: { id: 1, items: [], subtotal: '0' } })), + removeItem: vi.fn().mockReturnValue(of({ data: { id: 1, items: [], subtotal: '0' } })) + } + }, + { + provide: ToastService, + useValue: { + show: vi.fn(), + info: vi.fn(), + danger: vi.fn(), + success: vi.fn(), + dismiss: vi.fn() } } ] diff --git a/src/app/core/services/cart/cart.service.spec.ts b/src/app/core/services/cart/cart.service.spec.ts index ef040ea..1008407 100644 --- a/src/app/core/services/cart/cart.service.spec.ts +++ b/src/app/core/services/cart/cart.service.spec.ts @@ -91,8 +91,8 @@ describe('CartService', () => { const updatedCart = { ...mockCart, subtotal: '30.00' }; updatedCart.items[0].cantidad = 3; - service.updateItemQuantity(10, 3).subscribe((cart) => { - expect(cart).toEqual(updatedCart); + service.updateItemQuantity(10, 3).subscribe((res) => { + expect(res.data).toEqual(updatedCart); expect(service.cart()).toEqual(updatedCart); }); @@ -112,8 +112,8 @@ describe('CartService', () => { subtotal: '0.00' }; - service.removeItem(10).subscribe((cart) => { - expect(cart).toEqual(emptyCart); + service.removeItem(10).subscribe((res) => { + expect(res.data).toEqual(emptyCart); expect(service.cart()).toEqual(emptyCart); }); diff --git a/src/app/core/services/cart/cart.service.ts b/src/app/core/services/cart/cart.service.ts index 16c3f20..33a931c 100644 --- a/src/app/core/services/cart/cart.service.ts +++ b/src/app/core/services/cart/cart.service.ts @@ -44,7 +44,7 @@ export class CartService { ); } - updateItemQuantity(productVariantId: number, cantidad: number): Observable { + updateItemQuantity(productVariantId: number, cantidad: number): Observable> { return this.http .patch>( `${this.tenantApiUrl}/cart/items/${productVariantId}`, @@ -52,20 +52,18 @@ export class CartService { { withCredentials: true } ) .pipe( - map((response) => response.data), - tap((cart) => this.cartState.set(cart)) + tap((response) => this.cartState.set(response.data)) ); } - removeItem(productVariantId: number): Observable { + removeItem(productVariantId: number): Observable> { return this.http .delete>( `${this.tenantApiUrl}/cart/items/${productVariantId}`, { withCredentials: true } ) .pipe( - map((response) => response.data), - tap((cart) => this.cartState.set(cart)) + tap((response) => this.cartState.set(response.data)) ); } } diff --git a/src/app/shared/components/cart/cart.component.html b/src/app/shared/components/cart/cart.component.html index 1bfa5cc..52b9f15 100644 --- a/src/app/shared/components/cart/cart.component.html +++ b/src/app/shared/components/cart/cart.component.html @@ -10,7 +10,7 @@
- @for (item of items(); track item.product + item.discountedPrice) { + @for (item of items(); track item.product + item.discountedPrice + resetKey(); let idx = $index) { }
diff --git a/src/app/shared/components/cart/cart.component.ts b/src/app/shared/components/cart/cart.component.ts index d3b29ee..85e6531 100644 --- a/src/app/shared/components/cart/cart.component.ts +++ b/src/app/shared/components/cart/cart.component.ts @@ -1,5 +1,11 @@ -import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, inject, input, output, signal } from '@angular/core'; +import { HttpErrorResponse } from '@angular/common/http'; +import { Subject, EMPTY } from 'rxjs'; +import { catchError, debounceTime, groupBy, mergeMap, switchMap, tap } from 'rxjs'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { CartItemAttribute, CartItemComponent } from '../cart-item/cart-item.component'; +import { CartService } from '../../../core/services/cart/cart.service'; +import { ToastService } from '../../../core/services/toast.service'; export interface CartItemMock { imageUrl: string | null; @@ -20,6 +26,10 @@ export interface CartItemMock { changeDetection: ChangeDetectionStrategy.OnPush }) export class CartComponent { + private readonly cartService = inject(CartService); + private readonly toastService = inject(ToastService); + private readonly quantityUpdates$ = new Subject<{ productVariantId: number; quantity: number }>(); + readonly title = input('CARRITO'); readonly showClose = input(false); readonly items = input([]); @@ -30,6 +40,60 @@ export class CartComponent { readonly closed = output(); + protected readonly resetKey = signal(0); + + constructor() { + this.quantityUpdates$.pipe( + groupBy(update => update.productVariantId), + mergeMap(group$ => group$.pipe( + debounceTime(1000), + switchMap(update => this.cartService.updateItemQuantity(update.productVariantId, update.quantity).pipe( + tap({ + next: (res) => { + const msg = res.message || 'Cantidad de producto actualizada.'; + this.toastService.success(msg); + }, + error: (err: HttpErrorResponse) => { + console.error('Error updating cart quantity', err); + const msg = err.error?.message || 'Error al actualizar la cantidad del producto.'; + this.toastService.danger(msg); + this.resetKey.update(k => k + 1); + } + }), + catchError(() => EMPTY) + )) + )), + takeUntilDestroyed() + ).subscribe(); + } + + protected onItemQuantityChange(index: number, newQuantity: number): void { + const item = this.cartService.cart()?.items[index]; + if (item) { + this.quantityUpdates$.next({ + productVariantId: item.product_variant_id, + quantity: newQuantity + }); + } + } + + protected onItemRemove(index: number): void { + const item = this.cartService.cart()?.items[index]; + if (item) { + this.cartService.removeItem(item.product_variant_id).subscribe({ + next: (res) => { + const msg = res.message || 'Producto eliminado del carrito.'; + this.toastService.success(msg); + }, + error: (err: HttpErrorResponse) => { + console.error('Error removing item from cart', err); + const msg = err.error?.message || 'Error al eliminar el producto del carrito.'; + this.toastService.danger(msg); + } + }); + } + } + protected readonly formattedSubtotal = computed(() => this.formatCurrency(this.subtotal())); protected readonly formattedDiscount = computed(() => this.formatCurrency(this.discount())); protected readonly formattedTotal = computed(() => this.formatCurrency(this.total())); @@ -41,3 +105,4 @@ export class CartComponent { return `$ ${parts.join(',')}`; } } +