feat: implement direct purchase functionality and enhance product buy events

This commit is contained in:
2026-07-27 16:25:38 -03:00
parent 902f65d8d0
commit f063bac527
9 changed files with 140 additions and 12 deletions

View File

@@ -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>;
} }

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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)"
/> />
} }
} }

View File

@@ -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 });
}
} }

View File

@@ -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>

View File

@@ -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".
*/ */

View File

@@ -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>

View File

@@ -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('.');