diff --git a/src/app/core/services/catalog/catalog.interface.ts b/src/app/core/services/catalog/catalog.interface.ts index e586e42..4d8b4d3 100644 --- a/src/app/core/services/catalog/catalog.interface.ts +++ b/src/app/core/services/catalog/catalog.interface.ts @@ -66,7 +66,7 @@ export interface CatalogItemVariant { event_date_id?: number | null; event_date_ids?: number[]; event_dates?: string[]; - stock_tecnico: number | null; + maximum_addable_quantity?: number | null; minimum_use_date?: string | null; maximum_use_date?: string | null; effective_minimum_use_date?: string | null; @@ -98,7 +98,7 @@ export interface CatalogItemDetail { attributes: ProductAttribute[]; variants: CatalogItemVariant[]; selected_variant?: SelectedCatalogItemVariant; - stock_tecnico?: number | null; + maximum_addable_quantity?: number | null; images?: string[]; } @@ -113,7 +113,7 @@ export interface CatalogFeaturedItemVariant { id: number; descripcion?: string | null; precio?: string; - stock_tecnico: number | null; + maximum_addable_quantity?: number | null; values: Record; } @@ -145,7 +145,7 @@ export interface CatalogFeaturedItem { descripcion?: string | null; precio: number | string; image?: string | null; - stock_tecnico?: number | null; + maximum_addable_quantity?: number | null; variants?: CatalogFeaturedItemVariant[]; } diff --git a/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.ts b/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.ts index 594d44f..1e5e77e 100644 --- a/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.ts +++ b/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.ts @@ -232,7 +232,7 @@ export class ReutilizablesTestPageComponent { { id: 1001, precio: 250000, - stock_tecnico: 1, + maximum_addable_quantity: 1, values: { tipo: ticketOption('vip_lunch', 'VIP + Lunch'), sector: ticketOption('a', 'Sector A'), @@ -243,7 +243,7 @@ export class ReutilizablesTestPageComponent { { id: 1002, precio: 250000, - stock_tecnico: 1, + maximum_addable_quantity: 1, values: { tipo: ticketOption('vip_lunch', 'VIP + Lunch'), sector: ticketOption('a', 'Sector A'), @@ -254,7 +254,7 @@ export class ReutilizablesTestPageComponent { { id: 1003, precio: 250000, - stock_tecnico: 1, + maximum_addable_quantity: 1, values: { tipo: ticketOption('vip_lunch', 'VIP + Lunch'), sector: ticketOption('c', 'Sector C'), @@ -265,7 +265,7 @@ export class ReutilizablesTestPageComponent { { id: 1004, precio: 200000, - stock_tecnico: 1, + maximum_addable_quantity: 1, values: { tipo: ticketOption('vip_lunch', 'VIP + Lunch'), sector: ticketOption('a', 'Sector A'), @@ -276,7 +276,7 @@ export class ReutilizablesTestPageComponent { { id: 1005, precio: 200000, - stock_tecnico: 0, + maximum_addable_quantity: 0, values: { tipo: ticketOption('vip_lunch', 'VIP + Lunch'), sector: ticketOption('a', 'Sector A'), @@ -287,7 +287,7 @@ export class ReutilizablesTestPageComponent { { id: 1006, precio: 100000, - stock_tecnico: 1, + maximum_addable_quantity: 1, values: { tipo: ticketOption('general', 'General'), sector: ticketOption('b', 'Sector B'), @@ -298,7 +298,7 @@ export class ReutilizablesTestPageComponent { { id: 1007, precio: 100000, - stock_tecnico: 1, + maximum_addable_quantity: 1, values: { tipo: ticketOption('general', 'General'), sector: ticketOption('b', 'Sector B'), @@ -309,7 +309,7 @@ export class ReutilizablesTestPageComponent { { id: 1008, precio: 90000, - stock_tecnico: 1, + maximum_addable_quantity: 1, values: { tipo: ticketOption('general', 'General'), sector: ticketOption('d', 'Sector D'), @@ -320,7 +320,7 @@ export class ReutilizablesTestPageComponent { { id: 1009, precio: 65000, - stock_tecnico: 1, + maximum_addable_quantity: 1, values: { tipo: ticketOption('general', 'General'), sector: ticketOption('d', 'Sector D'), @@ -331,7 +331,7 @@ export class ReutilizablesTestPageComponent { { id: 1010, precio: 40000, - stock_tecnico: 1, + maximum_addable_quantity: 1, values: { tipo: ticketOption('general', 'General'), sector: ticketOption('d', 'Sector D'), diff --git a/src/app/features/store/components/product-attribute-selector/product-attribute-selector.component.spec.ts b/src/app/features/store/components/product-attribute-selector/product-attribute-selector.component.spec.ts index 4a7910c..c249803 100644 --- a/src/app/features/store/components/product-attribute-selector/product-attribute-selector.component.spec.ts +++ b/src/app/features/store/components/product-attribute-selector/product-attribute-selector.component.spec.ts @@ -31,7 +31,7 @@ describe('ProductAttributeSelectorComponent', () => { fixture.componentRef.setInput('variants', [ { id: 1, - stock_tecnico: null, + maximum_addable_quantity: null, values: { size: 'S' }, }, ]); @@ -51,12 +51,12 @@ describe('ProductAttributeSelectorComponent', () => { fixture.componentRef.setInput('variants', [ { id: 1, - stock_tecnico: 0, + maximum_addable_quantity: 0, values: { size: 'S' }, }, { id: 2, - stock_tecnico: 2, + maximum_addable_quantity: 2, values: { size: 'M' }, }, ]); @@ -92,9 +92,9 @@ describe('ProductAttributeSelectorComponent', () => { ]); fixture.componentRef.setInput('inventoryPolicy', 'unlimited'); fixture.componentRef.setInput('variants', [ - { id: 1, stock_tecnico: null, values: { event_date: '1' } }, - { id: 2, stock_tecnico: null, values: { event_date: '2' } }, - { id: 3, stock_tecnico: null, values: { event_date: ['1', '2'] } }, + { id: 1, maximum_addable_quantity: null, values: { event_date: '1' } }, + { id: 2, maximum_addable_quantity: null, values: { event_date: '2' } }, + { id: 3, maximum_addable_quantity: null, values: { event_date: ['1', '2'] } }, ]); fixture.detectChanges(); @@ -135,8 +135,8 @@ describe('ProductAttributeSelectorComponent', () => { ]); fixture.componentRef.setInput('inventoryPolicy', 'unlimited'); fixture.componentRef.setInput('variants', [ - { id: 1, stock_tecnico: null, values: { size: 'S', internal_type: 'adult' } }, - { id: 2, stock_tecnico: null, values: { size: 'M', internal_type: 'child' } }, + { id: 1, maximum_addable_quantity: null, values: { size: 'S', internal_type: 'adult' } }, + { id: 2, maximum_addable_quantity: null, values: { size: 'M', internal_type: 'child' } }, ]); fixture.detectChanges(); diff --git a/src/app/features/store/components/product-attribute-selector/product-attribute-selector.component.ts b/src/app/features/store/components/product-attribute-selector/product-attribute-selector.component.ts index d856169..0308d57 100644 --- a/src/app/features/store/components/product-attribute-selector/product-attribute-selector.component.ts +++ b/src/app/features/store/components/product-attribute-selector/product-attribute-selector.component.ts @@ -227,7 +227,7 @@ export class ProductAttributeSelectorComponent { } private isVariantAvailable(variant: CatalogItemVariant): boolean { - return this.inventoryPolicy() === 'unlimited' || (variant.stock_tecnico ?? 0) > 0; + return variant.maximum_addable_quantity !== 0; } private findFirstHexValue(value: unknown): string | null { 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 9c25820..7796679 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 @@ -1,5 +1,6 @@ import { TestBed, getTestBed } from '@angular/core/testing'; import { signal } from '@angular/core'; +import { HttpErrorResponse } from '@angular/common/http'; import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing'; import { ActivatedRoute, Router } from '@angular/router'; import { By } from '@angular/platform-browser'; @@ -36,7 +37,7 @@ describe('ProductDetailPageComponent', () => { has_tickets: false, minimum_use_date: null, maximum_use_date: null, - stock_tecnico: 10, + maximum_addable_quantity: 10, attributes: [], variants: [], }; @@ -190,10 +191,10 @@ describe('ProductDetailPageComponent', () => { const detailProduct: CatalogItemDetail = { ...mockProduct, images: ['https://example.com/product.png'], - variants: [{ id: 123, stock_tecnico: 10, values: {} }], + variants: [{ id: 123, maximum_addable_quantity: 10, values: {} }], selected_variant: { id: 123, - stock_tecnico: 10, + maximum_addable_quantity: 10, images: ['https://example.com/variant1.png', 'https://example.com/variant2.png'], values: {}, }, @@ -294,10 +295,12 @@ describe('ProductDetailPageComponent', () => { ], }, ], - variants: [{ id: 123, stock_tecnico: 10, values: { color: 'beige', material: 'Cuero' } }], + variants: [ + { id: 123, maximum_addable_quantity: 10, values: { color: 'beige', material: 'Cuero' } }, + ], selected_variant: { id: 123, - stock_tecnico: 10, + maximum_addable_quantity: 10, images: ['https://example.com/variant1.png'], values: { color: 'beige', @@ -334,8 +337,8 @@ describe('ProductDetailPageComponent', () => { purpose: 'entry', has_tickets: true, variants: [ - { id: 101, event_date_id: 20, stock_tecnico: 10, values: { event_date: '20' } }, - { id: 102, event_date_id: 21, stock_tecnico: 10, values: { event_date: '21' } }, + { id: 101, event_date_id: 20, maximum_addable_quantity: 10, values: { event_date: '20' } }, + { id: 102, event_date_id: 21, maximum_addable_quantity: 10, values: { event_date: '21' } }, ], attributes: [ { @@ -366,7 +369,7 @@ describe('ProductDetailPageComponent', () => { selected_variant: { id: 101, event_date_id: 20, - stock_tecnico: 10, + maximum_addable_quantity: 10, images: [], values: {}, }, @@ -410,7 +413,7 @@ describe('ProductDetailPageComponent', () => { fixture.componentInstance['selectedVariant'].set({ id: 1, - stock_tecnico: 10, + maximum_addable_quantity: 10, values: {}, }); fixture.detectChanges(); @@ -493,19 +496,45 @@ describe('ProductDetailPageComponent', () => { }); }); + it('shows the backend purchase-limit message for a direct checkout', async () => { + checkoutServiceStub.startCheckout.mockRejectedValue( + new HttpErrorResponse({ + status: 422, + error: { + code: 'purchase.limit_exceeded', + message: 'Podés agregar hasta 2 unidades más de “Auriculares Bluetooth”.', + maximum_addable_quantity: 2, + }, + }), + ); + await configureTestingModule(); + const fixture = TestBed.createComponent(ProductDetailPageComponent); + fixture.detectChanges(); + const buyButton = Array.from(fixture.nativeElement.querySelectorAll('app-button button')).find( + (button) => button.textContent?.trim() === 'Comprar', + ) as HTMLButtonElement; + + buyButton.click(); + await Promise.resolve(); + + expect(toastServiceStub.danger).toHaveBeenCalledWith( + 'Podés agregar hasta 2 unidades más de “Auriculares Bluetooth”.', + ); + }); + it('calls CartService.addItem when variant is selected and Add to Cart is clicked', async () => { const detailProduct: CatalogItemDetail = { ...mockProduct, variants: [ { id: 123, - stock_tecnico: 5, + maximum_addable_quantity: 5, values: {}, }, ], selected_variant: { id: 123, - stock_tecnico: 5, + maximum_addable_quantity: 5, images: [], values: {}, }, @@ -538,7 +567,7 @@ describe('ProductDetailPageComponent', () => { resolveProduct({ ...mockProduct, variants: [], - stock_tecnico: 4, + maximum_addable_quantity: 4, }); await configureTestingModule(); @@ -564,13 +593,13 @@ describe('ProductDetailPageComponent', () => { variants: [ { id: 123, - stock_tecnico: 5, + maximum_addable_quantity: 5, values: {}, }, ], selected_variant: { id: 123, - stock_tecnico: 5, + maximum_addable_quantity: 5, images: [], values: {}, }, @@ -602,7 +631,7 @@ describe('ProductDetailPageComponent', () => { it('allows unlimited variants to increase quantity without a maximum', async () => { const unlimitedVariant = { id: 321, - stock_tecnico: null, + maximum_addable_quantity: null, values: {}, }; resolveProduct({ @@ -610,7 +639,7 @@ describe('ProductDetailPageComponent', () => { inventory_policy: 'unlimited', selected_variant: { id: 321, - stock_tecnico: null, + maximum_addable_quantity: null, images: [], values: {}, }, @@ -634,7 +663,7 @@ describe('ProductDetailPageComponent', () => { it('caps an unlimited variant at the per-user purchase limit', async () => { const unlimitedVariant = { id: 322, - stock_tecnico: null, + maximum_addable_quantity: 2, values: {}, }; resolveProduct({ @@ -643,7 +672,7 @@ describe('ProductDetailPageComponent', () => { max_units_per_user: 2, selected_variant: { id: 322, - stock_tecnico: null, + maximum_addable_quantity: 2, images: [], values: { event_date: '20' }, }, @@ -667,14 +696,14 @@ describe('ProductDetailPageComponent', () => { it('disables purchase actions for tracked variants without stock', async () => { const trackedVariant = { id: 654, - stock_tecnico: 0, + maximum_addable_quantity: 0, values: {}, }; resolveProduct({ ...mockProduct, selected_variant: { id: 654, - stock_tecnico: 0, + maximum_addable_quantity: 0, images: [], values: {}, }, 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 254c760..831d7f3 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 @@ -98,27 +98,23 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy { const variant = this.selectedVariant(); if (!prod) return 0; - const stockLimit = variant - ? variant.stock_tecnico + return variant + ? (variant.maximum_addable_quantity ?? null) : prod.variants.length === 0 - ? (prod.stock_tecnico ?? null) + ? (prod.maximum_addable_quantity ?? null) : 0; - const userLimit = prod.max_units_per_user ?? null; - - if (stockLimit === null) return userLimit; - if (userLimit === null) return stockLimit; - return Math.min(stockLimit, userLimit); }); protected readonly selectedVariantAvailable = computed(() => { const prod = this.product(); if (!prod) return false; + if (this.selectedVariantMax() === 0) return false; const variant = this.selectedVariant(); - if (variant) return this.isVariantAvailable(variant, prod); + if (variant) return this.isVariantAvailable(variant); if (prod.purpose === 'entry') return false; if (prod.variants.length > 0) return false; - return prod.inventory_policy === 'unlimited' || (prod.stock_tecnico ?? 0) > 0; + return this.selectedVariantMax() !== 0; }); protected readonly descriptionExpanded = signal(false); protected readonly descriptionMaxHeight = signal(0); @@ -333,14 +329,18 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy { }); } catch (error) { console.error('Failed to create direct purchase:', error); - this.toastService.danger('No se pudo iniciar la compra directa.'); + const message = + error instanceof HttpErrorResponse && typeof error.error?.message === 'string' + ? error.error.message + : 'No se pudo iniciar la compra directa.'; + this.toastService.danger(message); } finally { this.creatingDirectPurchase.set(false); } } - private isVariantAvailable(variant: CatalogItemVariant, product: CatalogItemDetail): boolean { - return product.inventory_policy === 'unlimited' || (variant.stock_tecnico ?? 0) > 0; + private isVariantAvailable(variant: CatalogItemVariant): boolean { + return variant.maximum_addable_quantity !== 0; } protected toggleDescription(): void { diff --git a/src/app/features/store/pages/product-detail-page/product-detail-page.resolver.spec.ts b/src/app/features/store/pages/product-detail-page/product-detail-page.resolver.spec.ts index d378c5f..cac7e4d 100644 --- a/src/app/features/store/pages/product-detail-page/product-detail-page.resolver.spec.ts +++ b/src/app/features/store/pages/product-detail-page/product-detail-page.resolver.spec.ts @@ -28,7 +28,7 @@ describe('productDetailResolver', () => { has_tickets: false, minimum_use_date: null, maximum_use_date: null, - stock_tecnico: 0, + maximum_addable_quantity: 0, attributes: [], variants: [], }; diff --git a/src/app/features/store/pages/store-home-page/store-home-page.component.ts b/src/app/features/store/pages/store-home-page/store-home-page.component.ts index d0eb756..d4bf72b 100644 --- a/src/app/features/store/pages/store-home-page/store-home-page.component.ts +++ b/src/app/features/store/pages/store-home-page/store-home-page.component.ts @@ -224,6 +224,11 @@ export class StoreHomePageComponent implements OnInit, OnDestroy { return; } + if (error instanceof HttpErrorResponse && typeof error.error?.message === 'string') { + this.toastService.danger(error.error.message); + return; + } + this.toastService.danger('No se pudo iniciar la compra directa.'); } finally { this.creatingDirectPurchase.set(false); diff --git a/src/app/shared/components/product-list/product-list.component.html b/src/app/shared/components/product-list/product-list.component.html index 2e1d4a8..c181aba 100644 --- a/src/app/shared/components/product-list/product-list.component.html +++ b/src/app/shared/components/product-list/product-list.component.html @@ -12,6 +12,7 @@ [title]="item.nombre" [description]="item.descripcion ?? ''" [price]="price(item)" + [maximumAddableQuantity]="item.maximum_addable_quantity ?? null" [variants]="item.variants ?? []" [saving]="savingProductIds().has(item.id)" (buy)="emitRowBuy(item, $event)" @@ -23,6 +24,7 @@ [title]="item.nombre" [description]="item.descripcion ?? ''" [price]="price(item)" + [maximumAddableQuantity]="item.maximum_addable_quantity ?? null" [variants]="item.variants ?? []" [saving]="savingProductIds().has(item.id)" (buy)="emitColumnBuy(item, $event)" diff --git a/src/app/shared/components/product-list/product-list.component.spec.ts b/src/app/shared/components/product-list/product-list.component.spec.ts index 20516d0..32ecc20 100644 --- a/src/app/shared/components/product-list/product-list.component.spec.ts +++ b/src/app/shared/components/product-list/product-list.component.spec.ts @@ -193,8 +193,8 @@ describe('ProductListComponent', () => { const itemWithVariants: ProductListItem = { ...items[0], variants: [ - { id: 91, stock_tecnico: 3, values: { fecha: '10 de octubre' } }, - { id: 92, stock_tecnico: 4, values: { fecha: '11 de octubre' } }, + { id: 91, maximum_addable_quantity: 3, values: { fecha: '10 de octubre' } }, + { id: 92, maximum_addable_quantity: 4, values: { fecha: '11 de octubre' } }, ], }; const fixture = await render('column_with_cart', [itemWithVariants]); @@ -214,6 +214,58 @@ describe('ProductListComponent', () => { }); }); + it('caps the quantity at the selected variant maximum', async () => { + const itemWithVariants: ProductListItem = { + ...items[0], + variants: [ + { + id: 91, + maximum_addable_quantity: 2, + values: { fecha: '10 de octubre' }, + }, + ], + }; + const fixture = await render('column_with_cart', [itemWithVariants]); + await fixture.whenStable(); + fixture.detectChanges(); + const increase = fixture.nativeElement.querySelector( + '.quantity-selector__button:last-child', + ) as HTMLButtonElement; + + increase.click(); + fixture.detectChanges(); + + expect( + (fixture.nativeElement as HTMLElement).querySelector('.quantity-selector__value') + ?.textContent, + ).toContain('2'); + expect(increase.disabled).toBe(true); + }); + + it('disables purchase actions when the selected variant maximum is zero', async () => { + const unavailableItem: ProductListItem = { + ...items[0], + variants: [ + { + id: 91, + maximum_addable_quantity: 0, + values: { fecha: '10 de octubre' }, + }, + ], + }; + const fixture = await render('row', [unavailableItem]); + await fixture.whenStable(); + fixture.detectChanges(); + const buttons = Array.from( + (fixture.nativeElement as HTMLElement).querySelectorAll( + '.product-row-card__buttons button', + ), + ); + + expect(buttons).toHaveLength(2); + expect(buttons.every((button) => button.disabled)).toBe(true); + }); + it('renders pagination and emits the requested page', async () => { const fixture = await render('row'); const pageChangeSpy = vi.fn(); @@ -252,7 +304,7 @@ describe('ProductListComponent', () => { { id: 401, precio: '10000.00', - stock_tecnico: 1, + maximum_addable_quantity: 1, values: { tipo: { value: 'vip', label: 'VIP' }, sector: { value: 'a', label: 'Sector A' }, @@ -263,7 +315,7 @@ describe('ProductListComponent', () => { { id: 402, precio: '12000.00', - stock_tecnico: 1, + maximum_addable_quantity: 1, values: { tipo: { value: 'vip', label: 'VIP' }, sector: { value: 'a', label: 'Sector A' }, diff --git a/src/app/shared/components/product-row-card/product-row-card.component.html b/src/app/shared/components/product-row-card/product-row-card.component.html index dde80d0..7574dab 100644 --- a/src/app/shared/components/product-row-card/product-row-card.component.html +++ b/src/app/shared/components/product-row-card/product-row-card.component.html @@ -21,7 +21,11 @@ [(selectedVariant)]="selectedVariant" /> - + @@ -31,10 +35,16 @@
- Comprar + + Comprar +
- + {{ saving() ? 'Guardando' : 'Agregar al carrito' }}
diff --git a/src/app/shared/components/product-row-card/product-row-card.component.ts b/src/app/shared/components/product-row-card/product-row-card.component.ts index d9de24d..59dd265 100644 --- a/src/app/shared/components/product-row-card/product-row-card.component.ts +++ b/src/app/shared/components/product-row-card/product-row-card.component.ts @@ -1,4 +1,12 @@ -import { ChangeDetectionStrategy, Component, computed, input, model, output } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + computed, + effect, + input, + model, + output, +} from '@angular/core'; import { ButtonComponent } from '../button/button.component'; import { QuantitySelectorComponent } from '../quantity-selector/quantity-selector.component'; import { @@ -10,7 +18,7 @@ export interface Variant extends VariantSelectorVariant { label?: string; descripcion?: string | null; precio?: string | number; - stock_tecnico?: number | null; + maximum_addable_quantity?: number | null; } @Component({ @@ -26,6 +34,7 @@ export class ProductRowCardComponent { readonly title = input(''); readonly description = input(''); readonly price = input(0); + readonly maximumAddableQuantity = input(null); readonly variants = input([]); readonly saving = input(false); @@ -48,11 +57,25 @@ export class ProductRowCardComponent { return Number.isFinite(variantPrice) ? variantPrice : this.price(); }); + protected readonly effectiveMaximum = computed( + () => this.selectedVariantData()?.maximum_addable_quantity ?? this.maximumAddableQuantity(), + ); + protected readonly unavailable = computed(() => this.effectiveMaximum() === 0); readonly formattedPrice = computed(() => this.formatCurrency(this.effectivePrice())); + constructor() { + effect(() => { + const maximum = this.effectiveMaximum(); + + if (maximum !== null && maximum > 0 && this.quantity() > maximum) { + this.quantity.set(maximum); + } + }); + } + protected onAddToCart(): void { - if (this.saving()) { + if (this.saving() || this.unavailable()) { return; } @@ -63,6 +86,8 @@ export class ProductRowCardComponent { } protected onBuy(): void { + if (this.unavailable()) return; + this.buy.emit({ quantity: this.quantity(), variant: this.selectedVariant(), diff --git a/src/app/shared/components/product-ticket-selector/product-ticket-selector.component.ts b/src/app/shared/components/product-ticket-selector/product-ticket-selector.component.ts index 5803f48..e643317 100644 --- a/src/app/shared/components/product-ticket-selector/product-ticket-selector.component.ts +++ b/src/app/shared/components/product-ticket-selector/product-ticket-selector.component.ts @@ -361,7 +361,7 @@ export class ProductTicketSelectorComponent { return { id: item.variant.id, precio: item.variant.precio, - stock_tecnico: item.variant.stock_tecnico, + maximum_addable_quantity: item.variant.stock_tecnico, values: item.variant.values, }; } diff --git a/src/app/shared/components/product-vertical-with-cart-card/product-vertical-with-cart-card.component.html b/src/app/shared/components/product-vertical-with-cart-card/product-vertical-with-cart-card.component.html index 4d53809..84c28f8 100644 --- a/src/app/shared/components/product-vertical-with-cart-card/product-vertical-with-cart-card.component.html +++ b/src/app/shared/components/product-vertical-with-cart-card/product-vertical-with-cart-card.component.html @@ -11,13 +11,21 @@ @if (!hasVariants()) {
{{ formattedPrice() }} - +
} @else {
{{ formattedPrice() }} - +
@@ -29,14 +37,14 @@
Comprar {{ saving() ? 'Guardando' : 'Agregar al carrito' }} diff --git a/src/app/shared/components/product-vertical-with-cart-card/product-vertical-with-cart-card.component.ts b/src/app/shared/components/product-vertical-with-cart-card/product-vertical-with-cart-card.component.ts index f1804fd..e904c64 100644 --- a/src/app/shared/components/product-vertical-with-cart-card/product-vertical-with-cart-card.component.ts +++ b/src/app/shared/components/product-vertical-with-cart-card/product-vertical-with-cart-card.component.ts @@ -1,4 +1,12 @@ -import { ChangeDetectionStrategy, Component, computed, input, model, output } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + computed, + effect, + input, + model, + output, +} from '@angular/core'; import { ButtonComponent } from '../button/button.component'; import { QuantitySelectorComponent } from '../quantity-selector/quantity-selector.component'; @@ -10,6 +18,7 @@ import { export interface VerticalCartVariant extends VariantSelectorVariant { descripcion?: string | null; precio?: string | number; + maximum_addable_quantity?: number | null; } @Component({ @@ -23,6 +32,7 @@ export class ProductVerticalWithCartCardComponent { readonly title = input(''); readonly description = input(''); readonly price = input(0); + readonly maximumAddableQuantity = input(null); readonly variants = input([]); readonly saving = input(false); @@ -45,9 +55,23 @@ export class ProductVerticalWithCartCardComponent { }); protected readonly formattedPrice = computed(() => this.formatCurrency(this.effectivePrice())); protected readonly hasVariants = computed(() => this.variants().length > 0); + protected readonly effectiveMaximum = computed( + () => this.selectedVariantData()?.maximum_addable_quantity ?? this.maximumAddableQuantity(), + ); + protected readonly unavailable = computed(() => this.effectiveMaximum() === 0); + + constructor() { + effect(() => { + const maximum = this.effectiveMaximum(); + + if (maximum !== null && maximum > 0 && this.quantity() > maximum) { + this.quantity.set(maximum); + } + }); + } protected onAddToCart(): void { - if (this.saving()) { + if (this.saving() || this.unavailable()) { return; } @@ -55,6 +79,8 @@ export class ProductVerticalWithCartCardComponent { } protected onBuy(): void { + if (this.unavailable()) return; + this.buy.emit({ quantity: this.quantity(), variant: this.selectedVariant() }); }