275 lines
8.5 KiB
TypeScript
275 lines
8.5 KiB
TypeScript
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<string>('CARRITO');
|
|
readonly showClose = input<boolean>(false);
|
|
readonly items = input<CartItemMock[]>([]);
|
|
readonly subtotal = input<number>(0);
|
|
readonly discount = input<number>(0);
|
|
readonly total = input<number>(0);
|
|
readonly backgroundColor = input<string>('#ffffff');
|
|
readonly readonly = input<boolean>(false);
|
|
readonly allowEditing = input<boolean>(false);
|
|
readonly allowRemove = input<boolean>(true);
|
|
readonly persistQuantityChanges = input<boolean>(true);
|
|
readonly editingDisabled = input<boolean>(false);
|
|
readonly editing = model<boolean>(false);
|
|
|
|
readonly closed = output<void>();
|
|
readonly itemQuantityChange = output<{
|
|
item: CartItemMock;
|
|
index: number;
|
|
quantity: number;
|
|
}>();
|
|
|
|
protected readonly quantityOverrides = signal<Record<number, number>>({});
|
|
protected readonly variantOverrides = signal<Record<number, number>>({});
|
|
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);
|
|
},
|
|
});
|
|
}
|
|
}
|