feat: implement direct purchase functionality and enhance product buy events
This commit is contained in:
@@ -36,6 +36,10 @@ export type InventoryPolicy = 'tracked' | 'unlimited';
|
|||||||
export interface CatalogItemVariant {
|
export interface CatalogItemVariant {
|
||||||
id: number;
|
id: number;
|
||||||
stock_tecnico: number | null;
|
stock_tecnico: number | null;
|
||||||
|
minimum_use_date?: string | null;
|
||||||
|
maximum_use_date?: string | null;
|
||||||
|
effective_minimum_use_date?: string | null;
|
||||||
|
effective_maximum_use_date?: string | null;
|
||||||
values: Record<string, string>;
|
values: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import { catchError, distinctUntilChanged, map, of, switchMap, tap } from 'rxjs'
|
|||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
|
|
||||||
import { CartService } from '../../../../core/services/cart/cart.service';
|
import { CartService } from '../../../../core/services/cart/cart.service';
|
||||||
|
import { AuthService } from '../../../../core/services/auth/auth.service';
|
||||||
|
import { CheckoutService } from '../../../../core/services/checkout.service';
|
||||||
import {
|
import {
|
||||||
CatalogFeaturedItem,
|
CatalogFeaturedItem,
|
||||||
CatalogFeaturedItems,
|
CatalogFeaturedItems,
|
||||||
@@ -24,6 +26,7 @@ import { TenantService } from '../../../../core/services/tenant.service';
|
|||||||
import { ToastService } from '../../../../core/services/toast.service';
|
import { ToastService } from '../../../../core/services/toast.service';
|
||||||
import {
|
import {
|
||||||
ProductListCartEvent,
|
ProductListCartEvent,
|
||||||
|
ProductListBuyEvent,
|
||||||
ProductListComponent,
|
ProductListComponent,
|
||||||
ProductListItem,
|
ProductListItem,
|
||||||
} from '../../../../shared/components/product-list/product-list.component';
|
} from '../../../../shared/components/product-list/product-list.component';
|
||||||
@@ -43,7 +46,9 @@ interface SearchRouteState {
|
|||||||
export class SearchPageComponent {
|
export class SearchPageComponent {
|
||||||
private readonly minSearchLength = 3;
|
private readonly minSearchLength = 3;
|
||||||
private readonly cartService = inject(CartService);
|
private readonly cartService = inject(CartService);
|
||||||
|
private readonly authService = inject(AuthService);
|
||||||
private readonly catalogService = inject(CatalogService);
|
private readonly catalogService = inject(CatalogService);
|
||||||
|
private readonly checkoutService = inject(CheckoutService);
|
||||||
private readonly destroyRef = inject(DestroyRef);
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
private readonly route = inject(ActivatedRoute);
|
private readonly route = inject(ActivatedRoute);
|
||||||
private readonly router = inject(Router);
|
private readonly router = inject(Router);
|
||||||
@@ -54,6 +59,7 @@ export class SearchPageComponent {
|
|||||||
protected readonly results = signal<ApiPaginatedResponse<CatalogFeaturedItem[]> | null>(null);
|
protected readonly results = signal<ApiPaginatedResponse<CatalogFeaturedItem[]> | null>(null);
|
||||||
protected readonly loading = signal(false);
|
protected readonly loading = signal(false);
|
||||||
protected readonly error = signal<string | null>(null);
|
protected readonly error = signal<string | null>(null);
|
||||||
|
protected readonly creatingDirectPurchase = signal(false);
|
||||||
|
|
||||||
protected readonly productLayout = computed<CatalogProductLayout>(
|
protected readonly productLayout = computed<CatalogProductLayout>(
|
||||||
() => this.tenantService.tenant()?.search_product_layout ?? 'column_with_image',
|
() => this.tenantService.tenant()?.search_product_layout ?? 'column_with_image',
|
||||||
@@ -131,8 +137,45 @@ export class SearchPageComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onBuyProduct(product: ProductListItem): void {
|
protected async onBuyProduct(event: ProductListBuyEvent): Promise<void> {
|
||||||
void this.router.navigate(['/producto', product.id]);
|
if (!event.directPurchase) {
|
||||||
|
await this.router.navigate(['/producto', event.product.id]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.creatingDirectPurchase()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.authService.user()) {
|
||||||
|
await this.router.navigate(['/login'], {
|
||||||
|
queryParams: { returnUrl: `/producto/${event.product.id}` },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tenant = this.tenantService.tenant();
|
||||||
|
if (!tenant) {
|
||||||
|
this.toastService.danger('No se pudo identificar la tienda.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.creatingDirectPurchase.set(true);
|
||||||
|
try {
|
||||||
|
const purchase = await this.checkoutService.startCheckout(tenant.codigo, {
|
||||||
|
direct_item: {
|
||||||
|
catalog_item_id: event.product.id,
|
||||||
|
variant_id: event.variant ?? null,
|
||||||
|
cantidad: event.quantity,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await this.router.navigate(['/checkout'], { queryParams: { purchase: purchase.id } });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create direct purchase:', error);
|
||||||
|
this.toastService.danger('No se pudo iniciar la compra directa.');
|
||||||
|
} finally {
|
||||||
|
this.creatingDirectPurchase.set(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onAddToCart(event: ProductListCartEvent): void {
|
protected onAddToCart(event: ProductListCartEvent): void {
|
||||||
|
|||||||
@@ -12,13 +12,16 @@ import { ActivatedRoute, Router } from '@angular/router';
|
|||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
import { CartService } from '../../../../core/services/cart/cart.service';
|
import { CartService } from '../../../../core/services/cart/cart.service';
|
||||||
|
import { AuthService } from '../../../../core/services/auth/auth.service';
|
||||||
import { CatalogFeaturedGroup } from '../../../../core/services/catalog/catalog.interface';
|
import { CatalogFeaturedGroup } from '../../../../core/services/catalog/catalog.interface';
|
||||||
import { CatalogService } from '../../../../core/services/catalog/catalog.service';
|
import { CatalogService } from '../../../../core/services/catalog/catalog.service';
|
||||||
import { TenantService } from '../../../../core/services/tenant.service';
|
import { TenantService } from '../../../../core/services/tenant.service';
|
||||||
import { ToastService } from '../../../../core/services/toast.service';
|
import { ToastService } from '../../../../core/services/toast.service';
|
||||||
|
import { CheckoutService } from '../../../../core/services/checkout.service';
|
||||||
import {
|
import {
|
||||||
ProductListComponent,
|
ProductListComponent,
|
||||||
ProductListCartEvent,
|
ProductListCartEvent,
|
||||||
|
ProductListBuyEvent,
|
||||||
ProductListItem,
|
ProductListItem,
|
||||||
} from '../../../../shared/components/product-list/product-list.component';
|
} from '../../../../shared/components/product-list/product-list.component';
|
||||||
import { HeroBannerComponent } from '../../../../shared/components/hero-banner/hero-banner.component';
|
import { HeroBannerComponent } from '../../../../shared/components/hero-banner/hero-banner.component';
|
||||||
@@ -43,7 +46,9 @@ import {
|
|||||||
})
|
})
|
||||||
export class StoreHomePageComponent implements OnInit, OnDestroy {
|
export class StoreHomePageComponent implements OnInit, OnDestroy {
|
||||||
private readonly cartService = inject(CartService);
|
private readonly cartService = inject(CartService);
|
||||||
|
private readonly authService = inject(AuthService);
|
||||||
private readonly catalogService = inject(CatalogService);
|
private readonly catalogService = inject(CatalogService);
|
||||||
|
private readonly checkoutService = inject(CheckoutService);
|
||||||
private readonly route = inject(ActivatedRoute);
|
private readonly route = inject(ActivatedRoute);
|
||||||
private readonly router = inject(Router);
|
private readonly router = inject(Router);
|
||||||
private readonly tenantService = inject(TenantService);
|
private readonly tenantService = inject(TenantService);
|
||||||
@@ -55,6 +60,7 @@ export class StoreHomePageComponent implements OnInit, OnDestroy {
|
|||||||
protected readonly loadingGroupIds = signal<ReadonlySet<number>>(new Set());
|
protected readonly loadingGroupIds = signal<ReadonlySet<number>>(new Set());
|
||||||
protected readonly error = signal<string | null>(null);
|
protected readonly error = signal<string | null>(null);
|
||||||
protected readonly mainCarouselReady = signal(false);
|
protected readonly mainCarouselReady = signal(false);
|
||||||
|
protected readonly creatingDirectPurchase = signal(false);
|
||||||
protected readonly hasMainCarouselImages = computed(
|
protected readonly hasMainCarouselImages = computed(
|
||||||
() => (this.tenant()?.main_carousel_images?.length ?? 0) > 0,
|
() => (this.tenant()?.main_carousel_images?.length ?? 0) > 0,
|
||||||
);
|
);
|
||||||
@@ -118,8 +124,45 @@ export class StoreHomePageComponent implements OnInit, OnDestroy {
|
|||||||
this.groupRequestSubscriptions.set(groupId, subscription);
|
this.groupRequestSubscriptions.set(groupId, subscription);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onBuyProduct(product: ProductListItem): void {
|
protected async onBuyProduct(event: ProductListBuyEvent): Promise<void> {
|
||||||
this.router.navigate(['/producto', product.id]);
|
if (!event.directPurchase) {
|
||||||
|
await this.router.navigate(['/producto', event.product.id]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.creatingDirectPurchase()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.authService.user()) {
|
||||||
|
await this.router.navigate(['/login'], {
|
||||||
|
queryParams: { returnUrl: `/producto/${event.product.id}` },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tenant = this.tenantService.tenant();
|
||||||
|
if (!tenant) {
|
||||||
|
this.toastService.danger('No se pudo identificar la tienda.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.creatingDirectPurchase.set(true);
|
||||||
|
try {
|
||||||
|
const purchase = await this.checkoutService.startCheckout(tenant.codigo, {
|
||||||
|
direct_item: {
|
||||||
|
catalog_item_id: event.product.id,
|
||||||
|
variant_id: event.variant ?? null,
|
||||||
|
cantidad: event.quantity,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await this.router.navigate(['/checkout'], { queryParams: { purchase: purchase.id } });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create direct purchase:', error);
|
||||||
|
this.toastService.danger('No se pudo iniciar la compra directa.');
|
||||||
|
} finally {
|
||||||
|
this.creatingDirectPurchase.set(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onAddToCart(event: ProductListCartEvent): void {
|
protected onAddToCart(event: ProductListCartEvent): void {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
[description]="item.descripcion ?? ''"
|
[description]="item.descripcion ?? ''"
|
||||||
[price]="price(item)"
|
[price]="price(item)"
|
||||||
[variants]="variantsFor(item)"
|
[variants]="variantsFor(item)"
|
||||||
(buy)="buy.emit(item)"
|
(buy)="emitRowBuy(item, $event)"
|
||||||
(addToCart)="emitRowCart(item, $event)"
|
(addToCart)="emitRowCart(item, $event)"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
[title]="item.nombre"
|
[title]="item.nombre"
|
||||||
[description]="item.descripcion ?? ''"
|
[description]="item.descripcion ?? ''"
|
||||||
[price]="price(item)"
|
[price]="price(item)"
|
||||||
(buy)="buy.emit(item)"
|
(buy)="emitColumnBuy(item, $event)"
|
||||||
(addToCart)="emitColumnCart(item, $event)"
|
(addToCart)="emitColumnCart(item, $event)"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
[title]="item.nombre"
|
[title]="item.nombre"
|
||||||
[originalPrice]="price(item)"
|
[originalPrice]="price(item)"
|
||||||
[imagePriority]="loadImages() && index < 4"
|
[imagePriority]="loadImages() && index < 4"
|
||||||
(buy)="buy.emit(item)"
|
(buy)="emitProductDetailBuy(item)"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,13 @@ export interface ProductListCartEvent {
|
|||||||
variant?: number | null;
|
variant?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProductListBuyEvent {
|
||||||
|
product: ProductListItem;
|
||||||
|
quantity: number;
|
||||||
|
variant?: number | null;
|
||||||
|
directPurchase: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-product-list',
|
selector: 'app-product-list',
|
||||||
imports: [
|
imports: [
|
||||||
@@ -61,7 +68,7 @@ export class ProductListComponent {
|
|||||||
readonly loading = input(false);
|
readonly loading = input(false);
|
||||||
readonly loadImages = input(true);
|
readonly loadImages = input(true);
|
||||||
|
|
||||||
readonly buy = output<ProductListItem>();
|
readonly buy = output<ProductListBuyEvent>();
|
||||||
readonly addToCart = output<ProductListCartEvent>();
|
readonly addToCart = output<ProductListCartEvent>();
|
||||||
readonly pageChange = output<number>();
|
readonly pageChange = output<number>();
|
||||||
|
|
||||||
@@ -119,4 +126,24 @@ export class ProductListComponent {
|
|||||||
protected emitColumnCart(product: ProductListItem, event: { quantity: number }): void {
|
protected emitColumnCart(product: ProductListItem, event: { quantity: number }): void {
|
||||||
this.addToCart.emit({ product, quantity: event.quantity });
|
this.addToCart.emit({ product, quantity: event.quantity });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected emitRowBuy(
|
||||||
|
product: ProductListItem,
|
||||||
|
event: { quantity: number; variant: unknown },
|
||||||
|
): void {
|
||||||
|
this.buy.emit({
|
||||||
|
product,
|
||||||
|
quantity: event.quantity,
|
||||||
|
variant: typeof event.variant === 'number' ? event.variant : null,
|
||||||
|
directPurchase: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected emitColumnBuy(product: ProductListItem, event: { quantity: number }): void {
|
||||||
|
this.buy.emit({ product, quantity: event.quantity, variant: null, directPurchase: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected emitProductDetailBuy(product: ProductListItem): void {
|
||||||
|
this.buy.emit({ product, quantity: 1, variant: null, directPurchase: false });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
{{ formattedPrice() }}
|
{{ formattedPrice() }}
|
||||||
</span>
|
</span>
|
||||||
<div class="product-row-card__btn-wrapper">
|
<div class="product-row-card__btn-wrapper">
|
||||||
<app-button variant="primary" (click)="buy.emit()">
|
<app-button variant="primary" (click)="onBuy()">
|
||||||
Comprar
|
Comprar
|
||||||
</app-button>
|
</app-button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export class ProductRowCardComponent {
|
|||||||
readonly selectedVariant = model<any>(null);
|
readonly selectedVariant = model<any>(null);
|
||||||
|
|
||||||
// Interactive events
|
// Interactive events
|
||||||
readonly buy = output<void>();
|
readonly buy = output<{ quantity: number; variant: any }>();
|
||||||
readonly addToCart = output<{ quantity: number; variant: any }>();
|
readonly addToCart = output<{ quantity: number; variant: any }>();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -57,6 +57,13 @@ export class ProductRowCardComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected onBuy(): void {
|
||||||
|
this.buy.emit({
|
||||||
|
quantity: this.quantity(),
|
||||||
|
variant: this.selectedVariant(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper method to format currency numbers to Argentine Pesos style "$ XX.XXX".
|
* Helper method to format currency numbers to Argentine Pesos style "$ XX.XXX".
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="product-vertical-with-cart-card__actions">
|
<div class="product-vertical-with-cart-card__actions">
|
||||||
<app-button variant="primary" (click)="buy.emit()">Comprar</app-button>
|
<app-button variant="primary" (click)="onBuy()">Comprar</app-button>
|
||||||
<app-button variant="secondary" (click)="onAddToCart()"> Agregar al carrito </app-button>
|
<app-button variant="secondary" (click)="onAddToCart()"> Agregar al carrito </app-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export class ProductVerticalWithCartCardComponent {
|
|||||||
|
|
||||||
readonly quantity = model<number>(1);
|
readonly quantity = model<number>(1);
|
||||||
|
|
||||||
readonly buy = output<void>();
|
readonly buy = output<{ quantity: number }>();
|
||||||
readonly addToCart = output<{ quantity: number }>();
|
readonly addToCart = output<{ quantity: number }>();
|
||||||
|
|
||||||
protected readonly formattedPrice = computed(() => this.formatCurrency(this.price()));
|
protected readonly formattedPrice = computed(() => this.formatCurrency(this.price()));
|
||||||
@@ -26,6 +26,10 @@ export class ProductVerticalWithCartCardComponent {
|
|||||||
this.addToCart.emit({ quantity: this.quantity() });
|
this.addToCart.emit({ quantity: this.quantity() });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected onBuy(): void {
|
||||||
|
this.buy.emit({ quantity: this.quantity() });
|
||||||
|
}
|
||||||
|
|
||||||
private formatCurrency(value: number): string {
|
private formatCurrency(value: number): string {
|
||||||
const rounded = Math.round(value);
|
const rounded = Math.round(value);
|
||||||
const parts = rounded.toString().split('.');
|
const parts = rounded.toString().split('.');
|
||||||
|
|||||||
Reference in New Issue
Block a user