diff --git a/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.html b/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.html index 65afcf5..d1250b5 100644 --- a/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.html +++ b/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.html @@ -454,6 +454,39 @@ } + +
+
+
+

Cart copia

+

+ Copia aislada del carrito actual para iterar el rediseƱo sin tocar el componente productivo. +

+
+ + @if (!cartCopiaVisible) { + + Volver a mostrar cart copia + + } +
+ + @if (cartCopiaVisible) { +
+ +
+ } +
diff --git a/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.scss b/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.scss index fdb55e3..b62ca92 100644 --- a/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.scss +++ b/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.scss @@ -113,8 +113,8 @@ h1 { .cart-showcase__viewport { width: 100%; - max-width: 370px; - height: 360px; + max-width: 504px; + height: 640px; } .cart-showcase__header { diff --git a/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.ts b/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.ts index 026e994..7b3af24 100644 --- a/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.ts +++ b/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.ts @@ -4,6 +4,7 @@ import { CartComponent, CartItemMock } from '../../../../shared/components/cart/cart.component'; +import { CartCopiaComponent } from '../../../../shared/components/cart-copia/cart-copia.component'; import { CartIconComponent } from '../../../../shared/components/cart-icon/cart-icon.component'; import { IconButtonComponent } from '../../../../shared/components/icon-button/icon-button.component'; import { InputComponent } from '../../../../shared/components/input/input.component'; @@ -21,6 +22,7 @@ import { StepComponent } from '../../../../shared/components/stepper/step.compon imports: [ ButtonComponent, CartComponent, + CartCopiaComponent, InputComponent, PaginatorComponent, ProductCardComponent, @@ -58,6 +60,7 @@ export class ReutilizablesTestPageComponent { protected editableDisabled = true; protected currentPage = 1; protected cartVisible = true; + protected cartCopiaVisible = true; protected lastModalResult = 'Todavia no se abrio ningun modal.'; protected readonly paginatorTotalPages = 8; protected readonly cartBackgroundColor = '#ffffff'; @@ -160,6 +163,19 @@ export class ReutilizablesTestPageComponent { this.toastService.info('Se disparo el evento de cerrado del carrito mock.'); } + protected showCartCopia(): void { + this.cartCopiaVisible = true; + } + + protected hideCartCopia(): void { + this.cartCopiaVisible = false; + this.toastService.info('Se disparo el evento de cerrado del cart-copia mock.'); + } + + protected triggerCartCopiaCheckout(): void { + this.toastService.success('Se disparo la accion de compra del cart-copia mock.'); + } + protected triggerToast(type: 'success' | 'danger' | 'info'): void { if (type === 'success') { this.toastService.success( diff --git a/src/app/shared/components/cart-copia/cart-copia.component.html b/src/app/shared/components/cart-copia/cart-copia.component.html new file mode 100644 index 0000000..7620e2b --- /dev/null +++ b/src/app/shared/components/cart-copia/cart-copia.component.html @@ -0,0 +1,132 @@ +
+
+

{{ title() }}

+ + @if (showClose()) { + + } +
+ +
+
+ @for (item of items(); track item.productVariantId || item.product + item.discountedPrice; let idx = $index) { +
+
+ @if (item.discountPercentage && item.discountPercentage > 0) { + -{{ item.discountPercentage }}% + } + + @if (item.imageUrl) { + + } @else { + + } +
+ +
+
+

{{ item.product }}

+ +
+ @if (item.originalPrice !== null) { + {{ formatCurrency(item.originalPrice) }} + } + + {{ formatCurrency(item.discountedPrice) }} +
+
+ +
+ @for (attribute of item.attributes; track attribute.label + attribute.value) { +
+ {{ attribute.label }}: + {{ attribute.value }} +
+ } +
+ +
+
+ + + {{ getItemQuantity(item) }} + + +
+ + +
+
+
+ } +
+
+ + +
diff --git a/src/app/shared/components/cart-copia/cart-copia.component.scss b/src/app/shared/components/cart-copia/cart-copia.component.scss new file mode 100644 index 0000000..9ad190e --- /dev/null +++ b/src/app/shared/components/cart-copia/cart-copia.component.scss @@ -0,0 +1,351 @@ +:host { + display: block; + width: 100%; + height: 100%; +} + +.cart-copia { + display: grid; + grid-template-rows: auto minmax(0, 1fr) auto; + height: 100%; + border: 1px solid #f0f0f0; + border-radius: 10px; + overflow: hidden; + background: #ffffff; + box-shadow: 0 18px 40px rgba(24, 30, 48, 0.08); +} + +.cart-copia__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 16px 16px 30px; + background: transparent; + border-bottom: 1px solid #f2f2f2; +} + +.cart-copia__title { + margin: 0; + color: #a6a6a6; + font-size: 13px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.cart-copia__close-button { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + padding: 0; + border: 0; + background: transparent; + color: #ababab; + font-size: 16px; + transition: color 0.2s ease; + + &:hover { + color: #7b7b7b; + } +} + +.cart-copia__body { + min-height: 0; + background: #f8f8f8; +} + +.cart-copia__items { + display: grid; + gap: 18px; + height: 100%; + min-height: 0; + overflow-y: auto; + padding: 16px 14px 20px 30px; + scrollbar-color: #d1d1d1 transparent; + scrollbar-width: thin; + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-thumb { + background: #d1d1d1; + border-radius: 999px; + } +} + +.cart-copia__item { + display: grid; + grid-template-columns: 88px minmax(0, 1fr); + gap: 14px; + padding-bottom: 18px; + border-bottom: 1px solid #ececec; +} + +.cart-copia__media { + position: relative; + width: 88px; + height: 98px; + overflow: hidden; + background: #d8d8d8; + + img, + .cart-copia__media-placeholder { + display: block; + width: 100%; + height: 100%; + } + + img { + object-fit: cover; + } +} + +.cart-copia__media-placeholder { + background: linear-gradient(180deg, #d9d9d9, #cecece); +} + +.cart-copia__discount-badge { + position: absolute; + top: 0; + left: 0; + z-index: 1; + padding: 4px 8px 3px; + background: #6173ff; + color: #ffffff; + font-size: 8px; + font-weight: 500; + line-height: 1; +} + +.cart-copia__content { + display: grid; + gap: 10px; + min-width: 0; +} + +.cart-copia__top-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 12px; + align-items: start; +} + +.cart-copia__product { + margin: 0; + color: #7a7a7a; + font-size: 13px; + font-weight: 400; + line-height: 1.28; + text-transform: uppercase; +} + +.cart-copia__prices { + display: grid; + justify-items: end; + gap: 2px; + white-space: nowrap; + line-height: 1; +} + +.cart-copia__original-price { + color: #b2b2b2; + font-size: 10px; + font-weight: 300; + text-decoration: line-through; +} + +.cart-copia__discounted-price { + color: #909090; + font-size: 14px; + font-weight: 700; +} + +.cart-copia__attributes { + display: grid; + gap: 4px; +} + +.cart-copia__attribute { + color: #727272; + font-size: 12px; + line-height: 1.2; +} + +.cart-copia__attribute-label { + font-weight: 400; +} + +.cart-copia__attribute-value { + font-weight: 700; +} + +.cart-copia__item-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 10px; + margin-top: 2px; +} + +.cart-copia__quantity-selector { + display: inline-grid; + grid-template-columns: 16px 1fr 16px; + align-items: center; + width: 52px; + height: 23px; + border: 1px solid #cdcdcd; + border-radius: 4px; + background: #ffffff; +} + +.cart-copia__quantity-button { + display: inline-flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + padding: 0; + border: 0; + background: transparent; + color: #6f6f6f; + font-size: 12px; + font-weight: 700; + line-height: 1; + + &:disabled { + color: #c4c4c4; + cursor: not-allowed; + } +} + +.cart-copia__quantity-value { + color: #666666; + font-size: 11px; + font-weight: 700; + line-height: 1; + text-align: center; + user-select: none; +} + +.cart-copia__remove-button { + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + padding: 0; + border: 0; + background: transparent; + color: #aaaaaa; + font-size: 14px; + transition: color 0.2s ease; + + &:hover { + color: #7b7b7b; + } +} + +.cart-copia__footer { + display: grid; + gap: 6px; + padding: 20px 30px 28px; + background: #ffffff; + border-top: 1px solid #f2f2f2; +} + +.cart-copia__summary-row, +.cart-copia__total-row { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 12px; +} + +.cart-copia__summary-label, +.cart-copia__summary-value { + color: #a9a9a9; + font-size: 13px; + line-height: 1.2; +} + +.cart-copia__summary-label { + font-weight: 300; +} + +.cart-copia__summary-value { + font-weight: 700; +} + +.cart-copia__divider { + height: 1px; + margin: 6px 0 2px; + background: #efefef; +} + +.cart-copia__total-row { + margin-top: 2px; +} + +.cart-copia__total-label, +.cart-copia__total-value { + color: #666666; + font-size: 16px; + font-weight: 700; + line-height: 1.2; +} + +.cart-copia__footer-actions { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; + margin-top: 20px; +} + +.cart-copia__footer-button { + min-height: 42px; + border-radius: 8px; + border: 1px solid transparent; + font-size: 14px; + font-weight: 700; + transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease; +} + +.cart-copia__footer-button--secondary { + border-color: #6173ff; + background: #ffffff; + color: #6173ff; +} + +.cart-copia__footer-button--primary { + background: #6173ff; + color: #ffffff; +} + +@media (max-width: 520px) { + .cart-copia__header, + .cart-copia__footer { + padding-left: 18px; + padding-right: 18px; + } + + .cart-copia__items { + padding-left: 18px; + padding-right: 10px; + } + + .cart-copia__item { + grid-template-columns: 76px minmax(0, 1fr); + } + + .cart-copia__media { + width: 76px; + height: 88px; + } + + .cart-copia__footer-actions { + grid-template-columns: minmax(0, 1fr); + } +} diff --git a/src/app/shared/components/cart-copia/cart-copia.component.spec.ts b/src/app/shared/components/cart-copia/cart-copia.component.spec.ts new file mode 100644 index 0000000..8339fde --- /dev/null +++ b/src/app/shared/components/cart-copia/cart-copia.component.spec.ts @@ -0,0 +1,274 @@ +import '@angular/compiler'; +import { signal } from '@angular/core'; +import { TestBed, getTestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { + BrowserTestingModule, + platformBrowserTesting +} from '@angular/platform-browser/testing'; +import { of, throwError } from 'rxjs'; +import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; + +import { CartService } from '../../../core/services/cart/cart.service'; +import { ModalService } from '../../../core/services/modal.service'; +import { ToastService } from '../../../core/services/toast.service'; +import { CartCopiaComponent } from './cart-copia.component'; + +describe('CartCopiaComponent', () => { + beforeAll(() => { + try { + getTestBed().initTestEnvironment( + BrowserTestingModule, + platformBrowserTesting() + ); + } catch { + // Test environment may already be initialized by another setup entrypoint. + } + }); + + afterEach(() => { + TestBed.resetTestingModule(); + }); + + it('opens a confirm delete modal before removing an item', async () => { + const removeItem = vi.fn().mockReturnValue(of({ message: 'Producto eliminado.' })); + const openConfirmDelete = vi.fn().mockReturnValue(of(true)); + + await TestBed.configureTestingModule({ + imports: [CartCopiaComponent], + providers: [ + { + provide: CartService, + useValue: { + cart: signal(null).asReadonly(), + updateItemQuantity: vi.fn(), + removeItem + } + }, + { + provide: ModalService, + useValue: { + openConfirmDelete + } + }, + { + provide: ToastService, + useValue: { + success: vi.fn(), + info: vi.fn(), + danger: vi.fn() + } + } + ] + }).compileComponents(); + + const fixture = TestBed.createComponent(CartCopiaComponent); + fixture.componentRef.setInput('items', [ + { + productVariantId: 10, + imageUrl: null, + product: 'Producto de prueba', + originalPrice: null, + discountedPrice: 1000, + discountPercentage: null, + attributes: [], + quantity: 1 + } + ]); + fixture.detectChanges(); + + fixture.debugElement.query(By.css('.cart-copia__remove-button')).triggerEventHandler('click'); + + expect(openConfirmDelete).toHaveBeenCalledWith({ + title: 'Eliminar producto', + content: + 'Se eliminara "Producto de prueba" del carrito. Esta accion no se puede deshacer.', + confirmLabel: 'Eliminar', + cancelLabel: 'Cancelar' + }); + expect(removeItem).toHaveBeenCalledWith(10); + }); + + it('does not remove the item when the delete confirmation is cancelled', async () => { + const removeItem = vi.fn().mockReturnValue(of({})); + const openConfirmDelete = vi.fn().mockReturnValue(of(false)); + + await TestBed.configureTestingModule({ + imports: [CartCopiaComponent], + providers: [ + { + provide: CartService, + useValue: { + cart: signal(null).asReadonly(), + updateItemQuantity: vi.fn(), + removeItem + } + }, + { + provide: ModalService, + useValue: { + openConfirmDelete + } + }, + { + provide: ToastService, + useValue: { + success: vi.fn(), + info: vi.fn(), + danger: vi.fn() + } + } + ] + }).compileComponents(); + + const fixture = TestBed.createComponent(CartCopiaComponent); + fixture.componentRef.setInput('items', [ + { + productVariantId: 10, + imageUrl: null, + product: 'Producto de prueba', + originalPrice: null, + discountedPrice: 1000, + discountPercentage: null, + attributes: [], + quantity: 1 + } + ]); + fixture.detectChanges(); + + fixture.debugElement.query(By.css('.cart-copia__remove-button')).triggerEventHandler('click'); + + expect(openConfirmDelete).toHaveBeenCalled(); + expect(removeItem).not.toHaveBeenCalled(); + }); + + it('optimistically updates quantity and rolls back on error', async () => { + vi.useFakeTimers(); + const updateItemQuantity = vi.fn().mockReturnValue(throwError(() => new Error('Error'))); + const danger = vi.fn(); + + await TestBed.configureTestingModule({ + imports: [CartCopiaComponent], + providers: [ + { + provide: CartService, + useValue: { + cart: signal(null).asReadonly(), + updateItemQuantity, + removeItem: vi.fn() + } + }, + { + provide: ModalService, + useValue: {} + }, + { + provide: ToastService, + useValue: { + success: vi.fn(), + info: vi.fn(), + danger + } + } + ] + }).compileComponents(); + + const fixture = TestBed.createComponent(CartCopiaComponent); + const component = fixture.componentInstance; + + const item = { + productVariantId: 10, + imageUrl: null, + product: 'Producto de prueba', + originalPrice: null, + discountedPrice: 1000, + discountPercentage: null, + attributes: [], + quantity: 1 + }; + + fixture.componentRef.setInput('items', [item]); + fixture.detectChanges(); + + const increaseButton = fixture.debugElement.query(By.css('.cart-copia__quantity-button--increase')); + increaseButton.triggerEventHandler('click'); + fixture.detectChanges(); + increaseButton.triggerEventHandler('click'); + fixture.detectChanges(); + + expect((component as any).getItemQuantity(item)).toBe(3); + + vi.advanceTimersByTime(1000); + fixture.detectChanges(); + + expect((component as any).getItemQuantity(item)).toBe(1); + expect(danger).toHaveBeenCalled(); + + vi.useRealTimers(); + }); + + it('optimistically updates quantity and clears override on success', async () => { + vi.useFakeTimers(); + const updateItemQuantity = vi.fn().mockReturnValue(of({ message: 'Success', data: {} })); + const success = vi.fn(); + + await TestBed.configureTestingModule({ + imports: [CartCopiaComponent], + providers: [ + { + provide: CartService, + useValue: { + cart: signal(null).asReadonly(), + updateItemQuantity, + removeItem: vi.fn() + } + }, + { + provide: ModalService, + useValue: {} + }, + { + provide: ToastService, + useValue: { + success, + info: vi.fn(), + danger: vi.fn() + } + } + ] + }).compileComponents(); + + const fixture = TestBed.createComponent(CartCopiaComponent); + const component = fixture.componentInstance; + + const item = { + productVariantId: 10, + imageUrl: null, + product: 'Producto de prueba', + originalPrice: null, + discountedPrice: 1000, + discountPercentage: null, + attributes: [], + quantity: 1 + }; + + fixture.componentRef.setInput('items', [item]); + fixture.detectChanges(); + + const increaseButton = fixture.debugElement.query(By.css('.cart-copia__quantity-button--increase')); + increaseButton.triggerEventHandler('click'); + fixture.detectChanges(); + increaseButton.triggerEventHandler('click'); + fixture.detectChanges(); + + expect((component as any).getItemQuantity(item)).toBe(3); + + vi.advanceTimersByTime(1000); + fixture.detectChanges(); + + expect((component as any).getItemQuantity(item)).toBe(1); + expect(success).toHaveBeenCalled(); + + vi.useRealTimers(); + }); +}); diff --git a/src/app/shared/components/cart-copia/cart-copia.component.ts b/src/app/shared/components/cart-copia/cart-copia.component.ts new file mode 100644 index 0000000..6c19c90 --- /dev/null +++ b/src/app/shared/components/cart-copia/cart-copia.component.ts @@ -0,0 +1,213 @@ +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 type { CartItemAttribute } from '../cart-item/cart-item.component'; +import { ModalService } from '../../../core/services/modal.service'; +import { CartService } from '../../../core/services/cart/cart.service'; +import { ToastService } from '../../../core/services/toast.service'; + +export interface CartCopiaItemMock { + productVariantId?: number; + imageUrl: string | null; + product: string; + originalPrice: number | null; + discountedPrice: number; + discountPercentage: number | null; + attributes: CartItemAttribute[]; + quantity: number; +} + +@Component({ + selector: 'app-cart-copia', + standalone: true, + imports: [], + templateUrl: './cart-copia.component.html', + styleUrl: './cart-copia.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class CartCopiaComponent { + private readonly cartService = inject(CartService); + private readonly modalService = inject(ModalService); + 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([]); + readonly subtotal = input(0); + readonly discount = input(0); + readonly total = input(0); + readonly backgroundColor = input('#ffffff'); + readonly primaryActionLabel = input('Comprar'); + readonly secondaryActionLabel = input('Seguir comprando'); + readonly showActions = input(true); + + readonly closed = output(); + readonly primaryAction = output(); + readonly secondaryAction = output(); + + protected readonly quantityOverrides = signal>({}); + + 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); + this.clearOverride(update.productVariantId); + }, + 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.clearOverride(update.productVariantId); + } + }), + catchError(() => EMPTY) + )) + )), + takeUntilDestroyed() + ).subscribe(); + } + + protected getItemQuantity(item: CartCopiaItemMock): number { + if (item.productVariantId !== undefined && this.quantityOverrides()[item.productVariantId] !== undefined) { + return this.quantityOverrides()[item.productVariantId]; + } + return item.quantity; + } + + protected increaseItemQuantity(index: number): void { + const item = this.items()[index]; + + if (!item) { + return; + } + + this.onItemQuantityChange(index, this.getItemQuantity(item) + 1); + } + + protected decreaseItemQuantity(index: number): void { + const item = this.items()[index]; + + if (!item) { + return; + } + + const nextQuantity = Math.max(1, this.getItemQuantity(item) - 1); + + if (nextQuantity !== this.getItemQuantity(item)) { + this.onItemQuantityChange(index, nextQuantity); + } + } + + private clearOverride(productVariantId: number): void { + this.quantityOverrides.update((overrides) => { + const copy = { ...overrides }; + delete copy[productVariantId]; + return copy; + }); + } + + protected onItemQuantityChange(index: number, newQuantity: number): void { + const mockItem = this.items()[index]; + const productVariantId = mockItem?.productVariantId; + if (productVariantId) { + this.quantityOverrides.update((overrides) => ({ + ...overrides, + [productVariantId]: newQuantity + })); + this.quantityUpdates$.next({ + productVariantId, + quantity: newQuantity + }); + } else { + const item = this.cartService.cart()?.items[index]; + if (item) { + this.quantityOverrides.update((overrides) => ({ + ...overrides, + [item.product_variant_id]: newQuantity + })); + this.quantityUpdates$.next({ + productVariantId: item.product_variant_id, + quantity: newQuantity + }); + } + } + } + + protected onItemRemove(index: number): void { + const target = this.resolveRemoveTarget(index); + + if (!target) { + return; + } + + this.modalService.openConfirmDelete({ + title: 'Eliminar producto', + content: `Se eliminara "${target.productName}" del carrito. Esta accion no se puede deshacer.`, + confirmLabel: 'Eliminar', + cancelLabel: 'Cancelar' + }).subscribe((confirmed) => { + if (confirmed) { + this.removeItem(target.productVariantId); + } + }); + } + + protected readonly formattedSubtotal = computed(() => this.formatCurrency(this.subtotal())); + protected readonly formattedDiscount = computed(() => this.formatCurrency(this.discount())); + protected readonly formattedTotal = computed(() => this.formatCurrency(this.total())); + + protected formatCurrency(value: number): string { + const rounded = Math.round(value); + const parts = rounded.toString().split('.'); + parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, '.'); + return `$ ${parts.join(',')}`; + } + + private resolveRemoveTarget( + index: number + ): { productVariantId: number; productName: string } | null { + const mockItem = this.items()[index]; + const productVariantId = mockItem?.productVariantId; + + if (productVariantId) { + return { + productVariantId, + productName: mockItem.product + }; + } + + const item = this.cartService.cart()?.items[index]; + + if (!item) { + return null; + } + + return { + productVariantId: item.product_variant_id, + productName: item.product?.nombre ?? 'este producto' + }; + } + + private removeItem(productVariantId: number): void { + this.cartService.removeItem(productVariantId).subscribe({ + next: (res) => { + const msg = res.message || 'Producto eliminado del carrito.'; + this.toastService.info(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); + } + }); + } +}