From 8b785fd6317375ade95ab4640f5246306952db6b Mon Sep 17 00:00:00 2001 From: ncoronel Date: Thu, 16 Jul 2026 10:02:24 -0300 Subject: [PATCH] feat(cart): refactor cart item structure and update related components for consistency --- .../store-layout/store-layout.component.ts | 41 ++++-- src/app/core/services/cart/cart.interface.ts | 6 +- .../core/services/cart/cart.service.spec.ts | 30 ++-- src/app/core/services/cart/cart.service.ts | 49 +++---- .../checkout-page/checkout-page.component.ts | 57 +++++--- .../product-detail-page.component.spec.ts | 2 +- .../product-detail-page.component.ts | 2 +- .../components/cart/cart.component.html | 21 ++- .../components/cart/cart.component.spec.ts | 95 ++++++------ .../shared/components/cart/cart.component.ts | 138 ++++++++++-------- 10 files changed, 246 insertions(+), 195 deletions(-) diff --git a/src/app/core/layout/store-layout/store-layout.component.ts b/src/app/core/layout/store-layout/store-layout.component.ts index f129093..aa41b4a 100644 --- a/src/app/core/layout/store-layout/store-layout.component.ts +++ b/src/app/core/layout/store-layout/store-layout.component.ts @@ -2,7 +2,11 @@ import { Component, computed, inject, OnInit, signal } from '@angular/core'; import { Router, RouterOutlet } from '@angular/router'; import { TenantService } from '../../services/tenant.service'; import { CartService } from '../../services/cart/cart.service'; -import { StoreFooterComponent, StoreFooterSection, StoreSocialLink } from './store-footer/store-footer.component'; +import { + StoreFooterComponent, + StoreFooterSection, + StoreSocialLink, +} from './store-footer/store-footer.component'; import { StoreHeaderComponent } from './store-header/store-header.component'; import { CartComponent, CartItemMock } from '../../../shared/components/cart/cart.component'; import { ButtonComponent } from '../../../shared/components/button/button.component'; @@ -11,9 +15,15 @@ import { AuthService } from '../../services/auth/auth.service'; @Component({ selector: 'app-store-layout', - imports: [RouterOutlet, StoreHeaderComponent, StoreFooterComponent, CartComponent, ButtonComponent], + imports: [ + RouterOutlet, + StoreHeaderComponent, + StoreFooterComponent, + CartComponent, + ButtonComponent, + ], templateUrl: './store-layout.component.html', - styleUrl: './store-layout.component.scss' + styleUrl: './store-layout.component.scss', }) export class StoreLayoutComponent implements OnInit { private readonly tenantService = inject(TenantService); @@ -57,14 +67,14 @@ export class StoreLayoutComponent implements OnInit { } return { - productVariantId: item.product_variant_id, + cartItemId: item.id, imageUrl: item.product?.imagen ?? null, product, originalPrice: null, discountedPrice: parseFloat(item.precio_unitario), discountPercentage: null, attributes, - quantity: item.cantidad + quantity: item.cantidad, }; } @@ -81,7 +91,7 @@ export class StoreLayoutComponent implements OnInit { ngOnInit(): void { this.cartService.loadCart().subscribe({ - error: (err) => console.error('Error loading cart', err) + error: (err) => console.error('Error loading cart', err), }); } @@ -92,7 +102,7 @@ export class StoreLayoutComponent implements OnInit { protected onLogoutClick(): void { this.authService.logout().subscribe({ next: () => void this.router.navigate(['/login']), - error: (err) => console.error('Error logging out', err) + error: (err) => console.error('Error logging out', err), }); } @@ -101,34 +111,33 @@ export class StoreLayoutComponent implements OnInit { void this.router.navigate(['/checkout']); } - protected readonly footerSections: StoreFooterSection[] = [ { heading: 'Cuenta', - links: ['Mi cuenta', 'Mis compras', 'Cerrar sesion'] + links: ['Mi cuenta', 'Mis compras', 'Cerrar sesion'], }, { heading: 'Ayuda', - links: ['Contacto', 'Ayuda', 'Preguntas frecuentes'] - } + links: ['Contacto', 'Ayuda', 'Preguntas frecuentes'], + }, ]; protected readonly socialLinks: StoreSocialLink[] = [ { label: 'Instagram', - iconClass: 'fa-brands fa-instagram' + iconClass: 'fa-brands fa-instagram', }, { label: 'WhatsApp', - iconClass: 'fa-brands fa-whatsapp' + iconClass: 'fa-brands fa-whatsapp', }, { label: 'Facebook', - iconClass: 'fa-brands fa-facebook' + iconClass: 'fa-brands fa-facebook', }, { label: 'LinkedIn', - iconClass: 'fa-brands fa-linkedin-in' - } + iconClass: 'fa-brands fa-linkedin-in', + }, ]; } diff --git a/src/app/core/services/cart/cart.interface.ts b/src/app/core/services/cart/cart.interface.ts index d4916aa..aa854b1 100644 --- a/src/app/core/services/cart/cart.interface.ts +++ b/src/app/core/services/cart/cart.interface.ts @@ -3,12 +3,14 @@ export interface CartItemProduct { imagen: string | null; } +export type BuyableType = 'variant' | 'bundle'; + export interface CartItem { id: number; cantidad: number; precio_unitario: string; - product_id: number; - product_variant_id: number; + buyable_type: BuyableType; + buyable_id: number; product: CartItemProduct | null; } diff --git a/src/app/core/services/cart/cart.service.spec.ts b/src/app/core/services/cart/cart.service.spec.ts index 099987b..a81c661 100644 --- a/src/app/core/services/cart/cart.service.spec.ts +++ b/src/app/core/services/cart/cart.service.spec.ts @@ -21,21 +21,21 @@ describe('CartService', () => { id: 1, cantidad: 2, precio_unitario: '10.00', - product_id: 5, - product_variant_id: 10, + buyable_type: 'variant', + buyable_id: 10, product: { nombre: 'Test Product (Size: M)', - imagen: null - } - } + imagen: null, + }, + }, ], - subtotal: '20.00' + subtotal: '20.00', }; beforeEach(() => { tenantServiceMock = { getTenantApiUrl: vi.fn().mockReturnValue('http://api.test/tenants/acme'), - tenant: vi.fn().mockReturnValue({ codigo: 'acme' }) + tenant: vi.fn().mockReturnValue({ codigo: 'acme' }), }; TestBed.configureTestingModule({ @@ -43,8 +43,8 @@ describe('CartService', () => { CartService, provideHttpClient(), provideHttpClientTesting(), - { provide: TenantService, useValue: tenantServiceMock } - ] + { provide: TenantService, useValue: tenantServiceMock }, + ], }); service = TestBed.inject(CartService); @@ -83,19 +83,23 @@ describe('CartService', () => { tenant_codigo: 'acme', status: 'active', items: [], - subtotal: '0.00' + subtotal: '0.00', }); }); it('should add item and update signal', () => { - service.addItem(10, 2).subscribe((res) => { + service.addItem('variant', 10, 2).subscribe((res) => { expect(res.data).toEqual(mockCart); expect(service.cart()).toEqual(mockCart); }); const req = httpMock.expectOne('http://api.test/tenants/acme/cart/items'); expect(req.request.method).toBe('POST'); - expect(req.request.body).toEqual({ product_variant_id: 10, cantidad: 2 }); + expect(req.request.body).toEqual({ + buyable_type: 'variant', + buyable_id: 10, + cantidad: 2, + }); expect(req.request.withCredentials).toBe(true); req.flush({ data: mockCart }); }); @@ -122,7 +126,7 @@ describe('CartService', () => { tenant_codigo: 'acme', status: 'active', items: [], - subtotal: '0.00' + subtotal: '0.00', }; service.removeItem(10).subscribe((res) => { diff --git a/src/app/core/services/cart/cart.service.ts b/src/app/core/services/cart/cart.service.ts index 12ee0ae..7a1d891 100644 --- a/src/app/core/services/cart/cart.service.ts +++ b/src/app/core/services/cart/cart.service.ts @@ -4,10 +4,10 @@ import { catchError, map, Observable, tap } from 'rxjs'; import { ApiResponse } from '../api-response.interface'; import { TenantService } from '../tenant.service'; -import { Cart } from './cart.interface'; +import { BuyableType, Cart } from './cart.interface'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class CartService { private readonly http = inject(HttpClient); @@ -29,29 +29,31 @@ export class CartService { tenant_codigo: this.tenantService.tenant()?.codigo ?? '', status: 'active', items: [], - subtotal: '0.00' + subtotal: '0.00', }); } loadCart(): Observable { return this.http .get>(`${this.tenantApiUrl}/cart`, { - withCredentials: true + withCredentials: true, }) .pipe( map((response) => response.data), - tap((cart) => this.cartState.set(cart)) + tap((cart) => this.cartState.set(cart)), ); } - addItem(productVariantId: number, cantidad: number): Observable> { + addItem( + buyableType: BuyableType, + buyableId: number, + cantidad: number, + ): Observable> { this.isUpdatingState.set(true); return this.http - .post>( - `${this.tenantApiUrl}/cart/items`, - { product_variant_id: productVariantId, cantidad }, - { withCredentials: true } - ) + .post< + ApiResponse + >(`${this.tenantApiUrl}/cart/items`, { buyable_type: buyableType, buyable_id: buyableId, cantidad }, { withCredentials: true }) .pipe( tap((response) => { this.cartState.set(response.data); @@ -60,18 +62,16 @@ export class CartService { catchError((error) => { this.isUpdatingState.set(false); throw error; - }) + }), ); } - updateItemQuantity(productVariantId: number, cantidad: number): Observable> { + updateItemQuantity(cartItemId: number, cantidad: number): Observable> { this.isUpdatingState.set(true); return this.http - .patch>( - `${this.tenantApiUrl}/cart/items/${productVariantId}`, - { cantidad }, - { withCredentials: true } - ) + .patch< + ApiResponse + >(`${this.tenantApiUrl}/cart/items/${cartItemId}`, { cantidad }, { withCredentials: true }) .pipe( tap((response) => { this.cartState.set(response.data); @@ -80,17 +80,16 @@ export class CartService { catchError((error) => { this.isUpdatingState.set(false); throw error; - }) + }), ); } - removeItem(productVariantId: number): Observable> { + removeItem(cartItemId: number): Observable> { this.isUpdatingState.set(true); return this.http - .delete>( - `${this.tenantApiUrl}/cart/items/${productVariantId}`, - { withCredentials: true } - ) + .delete< + ApiResponse + >(`${this.tenantApiUrl}/cart/items/${cartItemId}`, { withCredentials: true }) .pipe( tap((response) => { this.cartState.set(response.data); @@ -99,7 +98,7 @@ export class CartService { catchError((error) => { this.isUpdatingState.set(false); throw error; - }) + }), ); } } diff --git a/src/app/features/store/pages/checkout-page/checkout-page.component.ts b/src/app/features/store/pages/checkout-page/checkout-page.component.ts index c5e696f..9839f26 100644 --- a/src/app/features/store/pages/checkout-page/checkout-page.component.ts +++ b/src/app/features/store/pages/checkout-page/checkout-page.component.ts @@ -1,4 +1,14 @@ -import { ChangeDetectionStrategy, Component, computed, effect, inject, OnInit, signal, untracked, ViewChild } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + computed, + effect, + inject, + OnInit, + signal, + untracked, + ViewChild, +} from '@angular/core'; import { FormBuilder, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { startWith } from 'rxjs'; @@ -14,7 +24,13 @@ import { StepComponent } from '../../../../shared/components/stepper/step.compon import { StepperComponent } from '../../../../shared/components/stepper/stepper.component'; import { CheckoutDataStepComponent } from './checkout-data-step.component'; import { CheckoutPaymentStepComponent } from './checkout-payment-step.component'; -import { CheckoutForm, PaymentMethod, PaymentMethodOption, TransferAccount, TransferField } from './checkout-page.models'; +import { + CheckoutForm, + PaymentMethod, + PaymentMethodOption, + TransferAccount, + TransferField, +} from './checkout-page.models'; @Component({ selector: 'app-checkout-page', @@ -24,11 +40,11 @@ import { CheckoutForm, PaymentMethod, PaymentMethodOption, TransferAccount, Tran StepperComponent, StepComponent, CheckoutDataStepComponent, - CheckoutPaymentStepComponent + CheckoutPaymentStepComponent, ], templateUrl: './checkout-page.component.html', styleUrl: './checkout-page.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class CheckoutPageComponent implements OnInit { private readonly formBuilder = inject(FormBuilder); @@ -44,7 +60,7 @@ export class CheckoutPageComponent implements OnInit { nombre: ['', [Validators.required]], email: ['', [Validators.required, Validators.email]], dni: ['', [Validators.required]], - telefono: ['', [Validators.required]] + telefono: ['', [Validators.required]], }); protected readonly cartSubtotal = computed(() => { @@ -66,7 +82,7 @@ export class CheckoutPageComponent implements OnInit { protected readonly paymentMethods: ReadonlyArray = [ { id: 'qr', label: 'QR' }, { id: 'transfer', label: 'Transferencia' }, - { id: 'telepagos', label: 'TelePagos' } + { id: 'telepagos', label: 'TelePagos' }, ]; protected readonly selectedPaymentMethod = signal('qr'); protected readonly copiedTransferField = signal(null); @@ -74,13 +90,15 @@ export class CheckoutPageComponent implements OnInit { titular: 'Nombre y Apellido', entidad: 'TelePagos', cvu: '0000000000000000000000', - alias: 'telepagos.ar' + alias: 'telepagos.ar', }); protected readonly isCreatingPurchase = signal(false); protected readonly createdPurchaseId = signal(null); protected readonly isGeneratingIntent = signal(false); - protected readonly isPaymentLoading = computed(() => this.isGeneratingIntent() || this.cartService.isUpdating()); + protected readonly isPaymentLoading = computed( + () => this.isGeneratingIntent() || this.cartService.isUpdating(), + ); protected readonly qrData = signal(null); constructor() { @@ -88,7 +106,7 @@ export class CheckoutPageComponent implements OnInit { // We only want to trigger the intent generation when the cart changes. // So we track the cart, but untrack the other signals to prevent duplicate calls. const cart = this.cartService.cart(); - + untracked(() => { const purchaseId = this.createdPurchaseId(); if (cart && purchaseId) { @@ -108,7 +126,7 @@ export class CheckoutPageComponent implements OnInit { nombre: user.nombre_apellido, email: user.email, dni: user.dni ?? '', - telefono: user.telefono ?? '' + telefono: user.telefono ?? '', }); this.form.controls.nombre.disable(); this.form.controls.email.disable(); @@ -138,14 +156,14 @@ export class CheckoutPageComponent implements OnInit { } return { - productVariantId: item.product_variant_id, + cartItemId: item.id, imageUrl: item.product?.imagen ?? null, product, originalPrice: null, discountedPrice: parseFloat(item.precio_unitario), discountPercentage: null, attributes, - quantity: item.cantidad + quantity: item.cantidad, }; } @@ -174,12 +192,11 @@ export class CheckoutPageComponent implements OnInit { const response = await this.checkoutService.createPurchase(tenant.codigo, payload); this.createdPurchaseId.set(response.id); - + this.stepper.next(); - + // Auto trigger intent for default option void this.selectPaymentMethod(this.selectedPaymentMethod()); - } catch (error) { console.error('Failed to create purchase:', error); // Here we could show an alert or toast @@ -205,8 +222,12 @@ export class CheckoutPageComponent implements OnInit { this.isGeneratingIntent.set(true); try { - const response = await this.checkoutService.generatePaymentIntent(tenant.codigo, purchaseId, method); - + const response = await this.checkoutService.generatePaymentIntent( + tenant.codigo, + purchaseId, + method, + ); + if (method === 'qr' && response.qr_data?.qr_code) { this.qrData.set(response.qr_data.qr_code); } else if (method === 'transfer' && response.transfer_data) { @@ -214,7 +235,7 @@ export class CheckoutPageComponent implements OnInit { titular: response.transfer_data.titular, entidad: response.transfer_data.entidad, cvu: response.transfer_data.cvu, - alias: response.transfer_data.alias + alias: response.transfer_data.alias, }); } } catch (error) { diff --git a/src/app/features/store/pages/product-detail-page/product-detail-page.component.spec.ts b/src/app/features/store/pages/product-detail-page/product-detail-page.component.spec.ts index f19800a..97b8915 100644 --- a/src/app/features/store/pages/product-detail-page/product-detail-page.component.spec.ts +++ b/src/app/features/store/pages/product-detail-page/product-detail-page.component.spec.ts @@ -427,7 +427,7 @@ describe('ProductDetailPageComponent', () => { addToCartButton.click(); fixture.detectChanges(); - expect(cartServiceStub.addItem).toHaveBeenCalledWith(123, 3); + expect(cartServiceStub.addItem).toHaveBeenCalledWith('variant', 123, 3); expect(toastServiceStub.success).toHaveBeenCalledWith('Producto agregado al carrito'); }); 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 3ef8624..765c26d 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 @@ -229,7 +229,7 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy { } this.addingToCart.set(true); - this.cartService.addItem(variant.variant_id, this.quantity()).subscribe({ + this.cartService.addItem('variant', variant.variant_id, this.quantity()).subscribe({ next: (res) => { const msg = res.message || 'Producto agregado al carrito'; this.toastService.success(msg); diff --git a/src/app/shared/components/cart/cart.component.html b/src/app/shared/components/cart/cart.component.html index 604eb4f..2998087 100644 --- a/src/app/shared/components/cart/cart.component.html +++ b/src/app/shared/components/cart/cart.component.html @@ -1,16 +1,28 @@ -
+

{{ title() }}

@if (showClose()) { - }
- @for (item of items(); track item.productVariantId || item.product + item.discountedPrice; let idx = $index) { + @for ( + item of items(); + track item.cartItemId || item.product + item.discountedPrice; + let idx = $index + ) {
- - - diff --git a/src/app/shared/components/cart/cart.component.spec.ts b/src/app/shared/components/cart/cart.component.spec.ts index 9bbe29b..519fdcd 100644 --- a/src/app/shared/components/cart/cart.component.spec.ts +++ b/src/app/shared/components/cart/cart.component.spec.ts @@ -2,10 +2,7 @@ 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 { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing'; import { of, throwError } from 'rxjs'; import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; @@ -17,10 +14,7 @@ import { CartComponent } from './cart.component'; describe('CartComponent', () => { beforeAll(() => { try { - getTestBed().initTestEnvironment( - BrowserTestingModule, - platformBrowserTesting() - ); + getTestBed().initTestEnvironment(BrowserTestingModule, platformBrowserTesting()); } catch { // Test environment may already be initialized by another setup entrypoint. } @@ -42,38 +36,38 @@ describe('CartComponent', () => { useValue: { cart: signal(null).asReadonly(), updateItemQuantity: vi.fn(), - removeItem - } + removeItem, + }, }, { provide: ModalService, useValue: { - openConfirmDelete - } + openConfirmDelete, + }, }, { provide: ToastService, useValue: { success: vi.fn(), info: vi.fn(), - danger: vi.fn() - } - } - ] + danger: vi.fn(), + }, + }, + ], }).compileComponents(); const fixture = TestBed.createComponent(CartComponent); fixture.componentRef.setInput('items', [ { - productVariantId: 10, + cartItemId: 10, imageUrl: null, product: 'Producto de prueba', originalPrice: null, discountedPrice: 1000, discountPercentage: null, attributes: [], - quantity: 1 - } + quantity: 1, + }, ]); fixture.detectChanges(); @@ -81,10 +75,9 @@ describe('CartComponent', () => { expect(openConfirmDelete).toHaveBeenCalledWith({ title: 'Eliminar producto', - content: - 'Se eliminara "Producto de prueba" del carrito. Esta accion no se puede deshacer.', + content: 'Se eliminara "Producto de prueba" del carrito. Esta accion no se puede deshacer.', confirmLabel: 'Eliminar', - cancelLabel: 'Cancelar' + cancelLabel: 'Cancelar', }); expect(removeItem).toHaveBeenCalledWith(10); }); @@ -101,38 +94,38 @@ describe('CartComponent', () => { useValue: { cart: signal(null).asReadonly(), updateItemQuantity: vi.fn(), - removeItem - } + removeItem, + }, }, { provide: ModalService, useValue: { - openConfirmDelete - } + openConfirmDelete, + }, }, { provide: ToastService, useValue: { success: vi.fn(), info: vi.fn(), - danger: vi.fn() - } - } - ] + danger: vi.fn(), + }, + }, + ], }).compileComponents(); const fixture = TestBed.createComponent(CartComponent); fixture.componentRef.setInput('items', [ { - productVariantId: 10, + cartItemId: 10, imageUrl: null, product: 'Producto de prueba', originalPrice: null, discountedPrice: 1000, discountPercentage: null, attributes: [], - quantity: 1 - } + quantity: 1, + }, ]); fixture.detectChanges(); @@ -155,36 +148,36 @@ describe('CartComponent', () => { useValue: { cart: signal(null).asReadonly(), updateItemQuantity, - removeItem: vi.fn() - } + removeItem: vi.fn(), + }, }, { provide: ModalService, - useValue: {} + useValue: {}, }, { provide: ToastService, useValue: { success: vi.fn(), info: vi.fn(), - danger - } - } - ] + danger, + }, + }, + ], }).compileComponents(); const fixture = TestBed.createComponent(CartComponent); const component = fixture.componentInstance; const item = { - productVariantId: 10, + cartItemId: 10, imageUrl: null, product: 'Producto de prueba', originalPrice: null, discountedPrice: 1000, discountPercentage: null, attributes: [], - quantity: 1 + quantity: 1, }; fixture.componentRef.setInput('items', [item]); @@ -221,36 +214,36 @@ describe('CartComponent', () => { useValue: { cart: signal(null).asReadonly(), updateItemQuantity, - removeItem: vi.fn() - } + removeItem: vi.fn(), + }, }, { provide: ModalService, - useValue: {} + useValue: {}, }, { provide: ToastService, useValue: { success, info: vi.fn(), - danger: vi.fn() - } - } - ] + danger: vi.fn(), + }, + }, + ], }).compileComponents(); const fixture = TestBed.createComponent(CartComponent); const component = fixture.componentInstance; const item = { - productVariantId: 10, + cartItemId: 10, imageUrl: null, product: 'Producto de prueba', originalPrice: null, discountedPrice: 1000, discountPercentage: null, attributes: [], - quantity: 1 + quantity: 1, }; fixture.componentRef.setInput('items', [item]); diff --git a/src/app/shared/components/cart/cart.component.ts b/src/app/shared/components/cart/cart.component.ts index bea2ff2..f68b331 100644 --- a/src/app/shared/components/cart/cart.component.ts +++ b/src/app/shared/components/cart/cart.component.ts @@ -1,4 +1,12 @@ -import { ChangeDetectionStrategy, Component, computed, inject, input, output, signal } 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'; @@ -9,7 +17,7 @@ import { CartService } from '../../../core/services/cart/cart.service'; import { ToastService } from '../../../core/services/toast.service'; export interface CartItemMock { - productVariantId?: number; + cartItemId?: number; imageUrl: string | null; product: string; originalPrice: number | null; @@ -25,13 +33,13 @@ export interface CartItemMock { imports: [CartItemComponent], templateUrl: './cart.component.html', styleUrl: './cart.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class CartComponent { private readonly cartService = inject(CartService); private readonly modalService = inject(ModalService); private readonly toastService = inject(ToastService); - private readonly quantityUpdates$ = new Subject<{ productVariantId: number; quantity: number }>(); + private readonly quantityUpdates$ = new Subject<{ cartItemId: number; quantity: number }>(); readonly title = input('CARRITO'); readonly showClose = input(false); @@ -46,68 +54,75 @@ export class CartComponent { 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(); + this.quantityUpdates$ + .pipe( + groupBy((update) => update.cartItemId), + mergeMap((group$) => + group$.pipe( + debounceTime(1000), + switchMap((update) => + this.cartService.updateItemQuantity(update.cartItemId, update.quantity).pipe( + tap({ + next: (res) => { + const msg = res.message || 'Cantidad de producto actualizada.'; + this.toastService.success(msg); + this.clearOverride(update.cartItemId); + }, + 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.cartItemId); + }, + }), + catchError(() => EMPTY), + ), + ), + ), + ), + takeUntilDestroyed(), + ) + .subscribe(); } protected getItemQuantity(item: CartItemMock): number { - if (item.productVariantId !== undefined && this.quantityOverrides()[item.productVariantId] !== undefined) { - return this.quantityOverrides()[item.productVariantId]; + if (item.cartItemId !== undefined && this.quantityOverrides()[item.cartItemId] !== undefined) { + return this.quantityOverrides()[item.cartItemId]; } return item.quantity; } - private clearOverride(productVariantId: number): void { + private clearOverride(cartItemId: number): void { this.quantityOverrides.update((overrides) => { const copy = { ...overrides }; - delete copy[productVariantId]; + delete copy[cartItemId]; return copy; }); } protected onItemQuantityChange(index: number, newQuantity: number): void { const mockItem = this.items()[index]; - const productVariantId = mockItem?.productVariantId; - if (productVariantId) { + const cartItemId = mockItem?.cartItemId; + if (cartItemId) { this.quantityOverrides.update((overrides) => ({ ...overrides, - [productVariantId]: newQuantity + [cartItemId]: newQuantity, })); this.quantityUpdates$.next({ - productVariantId, - quantity: newQuantity + cartItemId, + quantity: newQuantity, }); } else { const item = this.cartService.cart()?.items[index]; if (item) { this.quantityOverrides.update((overrides) => ({ ...overrides, - [item.product_variant_id]: newQuantity + [item.id]: newQuantity, })); this.quantityUpdates$.next({ - productVariantId: item.product_variant_id, - quantity: newQuantity + cartItemId: item.id, + quantity: newQuantity, }); } } @@ -120,16 +135,18 @@ export class CartComponent { 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); - } - }); + 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.cartItemId); + } + }); } protected readonly formattedSubtotal = computed(() => this.formatCurrency(this.subtotal())); @@ -143,16 +160,14 @@ export class CartComponent { return `$ ${parts.join(',')}`; } - private resolveRemoveTarget( - index: number - ): { productVariantId: number; productName: string } | null { + private resolveRemoveTarget(index: number): { cartItemId: number; productName: string } | null { const mockItem = this.items()[index]; - const productVariantId = mockItem?.productVariantId; + const cartItemId = mockItem?.cartItemId; - if (productVariantId) { + if (cartItemId) { return { - productVariantId, - productName: mockItem.product + cartItemId, + productName: mockItem.product, }; } @@ -163,13 +178,13 @@ export class CartComponent { } return { - productVariantId: item.product_variant_id, - productName: item.product?.nombre ?? 'este producto' + cartItemId: item.id, + productName: item.product?.nombre ?? 'este producto', }; } - private removeItem(productVariantId: number): void { - this.cartService.removeItem(productVariantId).subscribe({ + private removeItem(cartItemId: number): void { + this.cartService.removeItem(cartItemId).subscribe({ next: (res) => { const msg = res.message || 'Producto eliminado del carrito.'; this.toastService.info(msg); @@ -178,8 +193,7 @@ export class CartComponent { console.error('Error removing item from cart', err); const msg = err.error?.message || 'Error al eliminar el producto del carrito.'; this.toastService.danger(msg); - } + }, }); } } -