import { ChangeDetectionStrategy, Component, computed, inject, input, model, 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 { ModalService } from '../../../core/services/modal.service'; import { CartService } from '../../../core/services/cart/cart.service'; import { ToastService } from '../../../core/services/toast.service'; import { VariantSelectorVariant } from '../variant-selector/variant-selector.component'; export interface CartItemMock { cartItemId?: number; imageUrl: string | null; product: string; originalPrice: number | null; discountedPrice: number; discountPercentage: number | null; attributes: CartItemAttribute[]; quantity: number; variantId?: number | null; variants?: VariantSelectorVariant[]; } @Component({ selector: 'app-cart', standalone: true, imports: [CartItemComponent], templateUrl: './cart.component.html', styleUrl: './cart.component.scss', 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<{ cartItemId: 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 readonly = input(false); readonly allowEditing = input(false); readonly allowRemove = input(true); readonly persistQuantityChanges = input(true); readonly editingDisabled = input(false); readonly editing = model(false); readonly closed = output(); readonly itemQuantityChange = output<{ item: CartItemMock; index: number; quantity: number; }>(); protected readonly quantityOverrides = signal>({}); protected readonly variantOverrides = signal>({}); constructor() { 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.cartItemId !== undefined && this.quantityOverrides()[item.cartItemId] !== undefined) { return this.quantityOverrides()[item.cartItemId]; } return item.quantity; } protected getItemVariant(item: CartItemMock): number | null { if (item.cartItemId !== undefined && this.variantOverrides()[item.cartItemId] !== undefined) { return this.variantOverrides()[item.cartItemId]; } return item.variantId ?? null; } private clearOverride(cartItemId: number): void { this.quantityOverrides.update((overrides) => { const copy = { ...overrides }; delete copy[cartItemId]; return copy; }); } protected onItemQuantityChange(index: number, newQuantity: number): void { const mockItem = this.items()[index]; if (!mockItem) { return; } this.itemQuantityChange.emit({ item: mockItem, index, quantity: newQuantity, }); if (!this.persistQuantityChanges()) { return; } const cartItemId = mockItem?.cartItemId; if (cartItemId) { this.quantityOverrides.update((overrides) => ({ ...overrides, [cartItemId]: newQuantity, })); this.quantityUpdates$.next({ cartItemId, quantity: newQuantity, }); } else { const item = this.cartService.cart()?.items[index]; if (item) { this.quantityOverrides.update((overrides) => ({ ...overrides, [item.id]: newQuantity, })); this.quantityUpdates$.next({ cartItemId: item.id, quantity: newQuantity, }); } } } protected onItemVariantChange(index: number, variantId: number): void { const item = this.items()[index]; const cartItemId = item?.cartItemId; if (!item || !cartItemId || variantId === this.getItemVariant(item)) return; this.variantOverrides.update((overrides) => ({ ...overrides, [cartItemId]: variantId })); this.cartService .updateItemVariant(cartItemId, this.getItemQuantity(item), variantId) .subscribe({ next: (response) => { this.clearVariantOverride(cartItemId); this.toastService.success(response.message || 'Variante actualizada.'); }, error: (error: HttpErrorResponse) => { console.error('Error updating cart item variant', error); this.clearVariantOverride(cartItemId); this.toastService.danger(error.error?.message || 'No se pudo actualizar la variante.'); }, }); } private clearVariantOverride(cartItemId: number): void { this.variantOverrides.update((overrides) => { const copy = { ...overrides }; delete copy[cartItemId]; return copy; }); } protected onItemRemove(index: number): void { const target = this.resolveRemoveTarget(index); if (!target) { return; } this.modalService .openConfirmDelete({ title: 'Eliminar producto', content: `Se eliminará “${target.productName}” del carrito. Esta acción no se puede deshacer.`, confirmLabel: 'Eliminar', cancelLabel: 'Cancelar', }) .subscribe((confirmed) => { if (confirmed) { this.removeItem(target.cartItemId); } }); } 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 toggleEditing(): void { if (this.editingDisabled()) { return; } const editing = !this.editing(); this.editing.set(editing); } private 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): { cartItemId: number; productName: string } | null { const mockItem = this.items()[index]; const cartItemId = mockItem?.cartItemId; if (cartItemId) { return { cartItemId, productName: mockItem.product, }; } const item = this.cartService.cart()?.items[index]; if (!item) { return null; } return { cartItemId: item.id, productName: item.product?.nombre ?? 'este producto', }; } private removeItem(cartItemId: number): void { this.cartService.removeItem(cartItemId).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); }, }); } }