feat(catalog): replace stock_tecnico with maximum_addable_quantity across components and services
This commit is contained in:
@@ -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)"
|
||||
|
||||
@@ -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<HTMLButtonElement>(
|
||||
'.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' },
|
||||
|
||||
@@ -21,7 +21,11 @@
|
||||
[(selectedVariant)]="selectedVariant"
|
||||
/>
|
||||
|
||||
<app-quantity-selector [(quantity)]="quantity"></app-quantity-selector>
|
||||
<app-quantity-selector
|
||||
[(quantity)]="quantity"
|
||||
[max]="effectiveMaximum()"
|
||||
[disabled]="unavailable()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span class="product-row-card__price text-primary">
|
||||
@@ -31,10 +35,16 @@
|
||||
|
||||
<div class="product-row-card__buttons">
|
||||
<div class="product-row-card__btn-wrapper">
|
||||
<app-button variant="primary" (click)="onBuy()"> Comprar </app-button>
|
||||
<app-button variant="primary" [disabled]="unavailable()" (click)="onBuy()">
|
||||
Comprar
|
||||
</app-button>
|
||||
</div>
|
||||
<div class="product-row-card__btn-wrapper">
|
||||
<app-button variant="secondary" [disabled]="saving()" (click)="onAddToCart()">
|
||||
<app-button
|
||||
variant="secondary"
|
||||
[disabled]="saving() || unavailable()"
|
||||
(click)="onAddToCart()"
|
||||
>
|
||||
{{ saving() ? 'Guardando' : 'Agregar al carrito' }}
|
||||
</app-button>
|
||||
</div>
|
||||
|
||||
@@ -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<string>('');
|
||||
readonly description = input<string>('');
|
||||
readonly price = input<number>(0);
|
||||
readonly maximumAddableQuantity = input<number | null>(null);
|
||||
readonly variants = input<Variant[]>([]);
|
||||
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(),
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,13 +11,21 @@
|
||||
@if (!hasVariants()) {
|
||||
<div class="product-vertical-with-cart-card__summary">
|
||||
<span class="product-vertical-with-cart-card__price">{{ formattedPrice() }}</span>
|
||||
<app-quantity-selector [(quantity)]="quantity" />
|
||||
<app-quantity-selector
|
||||
[(quantity)]="quantity"
|
||||
[max]="effectiveMaximum()"
|
||||
[disabled]="unavailable()"
|
||||
/>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="product-vertical-with-cart-card__variants">
|
||||
<div class="product-vertical-with-cart-card__summary">
|
||||
<span class="product-vertical-with-cart-card__price">{{ formattedPrice() }}</span>
|
||||
<app-quantity-selector [(quantity)]="quantity" />
|
||||
<app-quantity-selector
|
||||
[(quantity)]="quantity"
|
||||
[max]="effectiveMaximum()"
|
||||
[disabled]="unavailable()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="product-vertical-with-cart-card__variant-selectors">
|
||||
@@ -29,14 +37,14 @@
|
||||
<div class="product-vertical-with-cart-card__actions">
|
||||
<app-button
|
||||
variant="primary"
|
||||
[disabled]="hasVariants() && selectedVariant() === null"
|
||||
[disabled]="unavailable() || (hasVariants() && selectedVariant() === null)"
|
||||
(click)="onBuy()"
|
||||
>
|
||||
Comprar
|
||||
</app-button>
|
||||
<app-button
|
||||
variant="secondary"
|
||||
[disabled]="saving() || (hasVariants() && selectedVariant() === null)"
|
||||
[disabled]="saving() || unavailable() || (hasVariants() && selectedVariant() === null)"
|
||||
(click)="onAddToCart()"
|
||||
>
|
||||
{{ saving() ? 'Guardando' : 'Agregar al carrito' }}
|
||||
|
||||
@@ -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<string>('');
|
||||
readonly description = input<string>('');
|
||||
readonly price = input<number>(0);
|
||||
readonly maximumAddableQuantity = input<number | null>(null);
|
||||
readonly variants = input<VerticalCartVariant[]>([]);
|
||||
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() });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user