feat(catalog): consume global availability decisions
This commit is contained in:
@@ -1,45 +1,80 @@
|
|||||||
import { CatalogAvailability } from './catalog.interface';
|
import { CatalogAction, CatalogAvailability } from './catalog.interface';
|
||||||
|
|
||||||
|
const ALL_CATALOG_ACTIONS: CatalogAction[] = [
|
||||||
|
'select_variant',
|
||||||
|
'change_quantity',
|
||||||
|
'add_to_cart',
|
||||||
|
'buy_now',
|
||||||
|
];
|
||||||
|
|
||||||
export const AVAILABLE_CATALOG_AVAILABILITY: CatalogAvailability = {
|
export const AVAILABLE_CATALOG_AVAILABILITY: CatalogAvailability = {
|
||||||
subject: 'product',
|
state: 'visible',
|
||||||
maximum_quantity: null,
|
maximum_quantity: null,
|
||||||
restrictions: [],
|
allowed_actions: ALL_CATALOG_ACTIONS,
|
||||||
capabilities: {
|
reasons: [],
|
||||||
display: true,
|
|
||||||
select_variant: true,
|
|
||||||
change_quantity: true,
|
|
||||||
add_to_cart: true,
|
|
||||||
buy_now: true,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createCatalogAvailability(
|
export function createCatalogAvailability(maximumQuantity: number | null): CatalogAvailability {
|
||||||
maximumQuantity: number | null,
|
|
||||||
subject: CatalogAvailability['subject'] = 'product',
|
|
||||||
): CatalogAvailability {
|
|
||||||
const unavailable = maximumQuantity === 0;
|
const unavailable = maximumQuantity === 0;
|
||||||
|
|
||||||
|
if (unavailable) {
|
||||||
|
return {
|
||||||
|
state: 'hidden',
|
||||||
|
reasons: [
|
||||||
|
{
|
||||||
|
code: 'out_of_stock',
|
||||||
|
message: 'Este producto no tiene stock disponible.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subject,
|
state: 'visible',
|
||||||
maximum_quantity: maximumQuantity,
|
maximum_quantity: maximumQuantity,
|
||||||
restrictions: unavailable
|
allowed_actions: [...ALL_CATALOG_ACTIONS],
|
||||||
? [
|
reasons: [],
|
||||||
{
|
|
||||||
code: 'out_of_stock',
|
|
||||||
message: 'Este producto no tiene stock disponible.',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [],
|
|
||||||
capabilities: {
|
|
||||||
display: !unavailable,
|
|
||||||
select_variant: !unavailable,
|
|
||||||
change_quantity: !unavailable,
|
|
||||||
add_to_cart: !unavailable,
|
|
||||||
buy_now: !unavailable,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function primaryAvailabilityMessage(availability: CatalogAvailability): string | null {
|
export function primaryAvailabilityMessage(availability: CatalogAvailability): string | null {
|
||||||
return availability.restrictions[0]?.message ?? null;
|
return availability.reasons[0]?.message ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function allowsCatalogAction(
|
||||||
|
availability: CatalogAvailability,
|
||||||
|
action: CatalogAction,
|
||||||
|
): boolean {
|
||||||
|
return availability.state === 'visible' && availability.allowed_actions.includes(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function maximumCatalogQuantity(availability: CatalogAvailability): number | null {
|
||||||
|
return availability.state === 'visible' ? availability.maximum_quantity : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function combineCatalogAvailability(
|
||||||
|
product: CatalogAvailability,
|
||||||
|
variant?: CatalogAvailability | null,
|
||||||
|
): CatalogAvailability {
|
||||||
|
if (!variant) return product;
|
||||||
|
|
||||||
|
const reasons = [...product.reasons, ...variant.reasons];
|
||||||
|
if (product.state === 'hidden' || variant.state === 'hidden') {
|
||||||
|
return { state: 'hidden', reasons };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
state: 'visible',
|
||||||
|
maximum_quantity: minimumNullable(product.maximum_quantity, variant.maximum_quantity),
|
||||||
|
allowed_actions: product.allowed_actions.filter((action) =>
|
||||||
|
variant.allowed_actions.includes(action),
|
||||||
|
),
|
||||||
|
reasons,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function minimumNullable(left: number | null, right: number | null): number | null {
|
||||||
|
if (left === null) return right;
|
||||||
|
if (right === null) return left;
|
||||||
|
return Math.min(left, right);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,20 +54,19 @@ export interface CatalogRestriction {
|
|||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CatalogCapabilities {
|
export type CatalogAction = 'select_variant' | 'change_quantity' | 'add_to_cart' | 'buy_now';
|
||||||
display: boolean;
|
|
||||||
select_variant: boolean;
|
|
||||||
change_quantity: boolean;
|
|
||||||
add_to_cart: boolean;
|
|
||||||
buy_now: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CatalogAvailability {
|
export type CatalogAvailability =
|
||||||
subject: 'product' | 'variant';
|
| {
|
||||||
maximum_quantity: number | null;
|
state: 'hidden';
|
||||||
restrictions: CatalogRestriction[];
|
reasons: CatalogRestriction[];
|
||||||
capabilities: CatalogCapabilities;
|
}
|
||||||
}
|
| {
|
||||||
|
state: 'visible';
|
||||||
|
maximum_quantity: number | null;
|
||||||
|
allowed_actions: CatalogAction[];
|
||||||
|
reasons: CatalogRestriction[];
|
||||||
|
};
|
||||||
|
|
||||||
export interface CatalogVariantOption {
|
export interface CatalogVariantOption {
|
||||||
value: string;
|
value: string;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
ProductAttribute,
|
ProductAttribute,
|
||||||
ProductAttributeOption,
|
ProductAttributeOption,
|
||||||
} from '../../../../core/services/catalog/catalog.interface';
|
} from '../../../../core/services/catalog/catalog.interface';
|
||||||
|
import { allowsCatalogAction } from '../../../../core/services/catalog/catalog-availability';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-product-attribute-selector',
|
selector: 'app-product-attribute-selector',
|
||||||
@@ -242,7 +243,7 @@ export class ProductAttributeSelectorComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private isVariantAvailable(variant: CatalogItemVariant): boolean {
|
private isVariantAvailable(variant: CatalogItemVariant): boolean {
|
||||||
return variant.availability.capabilities.select_variant;
|
return allowsCatalogAction(variant.availability, 'select_variant');
|
||||||
}
|
}
|
||||||
|
|
||||||
private findFirstHexValue(value: unknown): string | null {
|
private findFirstHexValue(value: unknown): string | null {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
[variants]="prod.variants"
|
[variants]="prod.variants"
|
||||||
[selectedVariant]="prod.selected_variant ?? null"
|
[selectedVariant]="prod.selected_variant ?? null"
|
||||||
[inventoryPolicy]="prod.inventory_policy"
|
[inventoryPolicy]="prod.inventory_policy"
|
||||||
[disabled]="!prod.availability.capabilities.select_variant"
|
[disabled]="!allows(prod.availability, 'select_variant')"
|
||||||
(variantChange)="onVariantChange($event)"
|
(variantChange)="onVariantChange($event)"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
<app-quantity-selector
|
<app-quantity-selector
|
||||||
[(quantity)]="quantity"
|
[(quantity)]="quantity"
|
||||||
[max]="selectedVariantMax()"
|
[max]="selectedVariantMax()"
|
||||||
[disabled]="!effectiveAvailability().capabilities.change_quantity"
|
[disabled]="!allows(effectiveAvailability(), 'change_quantity')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="product-detail__actions">
|
<div class="product-detail__actions">
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ import { TenantService } from '../../../../core/services/tenant.service';
|
|||||||
import { AuthService } from '../../../../core/services/auth/auth.service';
|
import { AuthService } from '../../../../core/services/auth/auth.service';
|
||||||
import {
|
import {
|
||||||
AVAILABLE_CATALOG_AVAILABILITY,
|
AVAILABLE_CATALOG_AVAILABILITY,
|
||||||
|
allowsCatalogAction,
|
||||||
|
combineCatalogAvailability,
|
||||||
|
maximumCatalogQuantity,
|
||||||
primaryAvailabilityMessage,
|
primaryAvailabilityMessage,
|
||||||
} from '../../../../core/services/catalog/catalog-availability';
|
} from '../../../../core/services/catalog/catalog-availability';
|
||||||
|
|
||||||
@@ -105,7 +108,7 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy {
|
|||||||
const variant = this.selectedVariant();
|
const variant = this.selectedVariant();
|
||||||
if (!prod) return AVAILABLE_CATALOG_AVAILABILITY;
|
if (!prod) return AVAILABLE_CATALOG_AVAILABILITY;
|
||||||
|
|
||||||
return variant?.availability ?? prod.availability;
|
return combineCatalogAvailability(prod.availability, variant?.availability);
|
||||||
});
|
});
|
||||||
protected readonly selectedVariantMax = computed<number | null>(() => {
|
protected readonly selectedVariantMax = computed<number | null>(() => {
|
||||||
const prod = this.product();
|
const prod = this.product();
|
||||||
@@ -113,7 +116,7 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy {
|
|||||||
if (!prod) return 0;
|
if (!prod) return 0;
|
||||||
if (!variant && prod.variants.length > 0) return 0;
|
if (!variant && prod.variants.length > 0) return 0;
|
||||||
|
|
||||||
return this.effectiveAvailability().maximum_quantity;
|
return maximumCatalogQuantity(this.effectiveAvailability());
|
||||||
});
|
});
|
||||||
protected readonly hasPurchasableSelection = computed(() => {
|
protected readonly hasPurchasableSelection = computed(() => {
|
||||||
const prod = this.product();
|
const prod = this.product();
|
||||||
@@ -122,14 +125,19 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy {
|
|||||||
return prod.variants.length === 0 || this.selectedVariant() !== null;
|
return prod.variants.length === 0 || this.selectedVariant() !== null;
|
||||||
});
|
});
|
||||||
protected readonly canAddToCart = computed(
|
protected readonly canAddToCart = computed(
|
||||||
() => this.hasPurchasableSelection() && this.effectiveAvailability().capabilities.add_to_cart,
|
() =>
|
||||||
|
this.hasPurchasableSelection() &&
|
||||||
|
allowsCatalogAction(this.effectiveAvailability(), 'add_to_cart'),
|
||||||
);
|
);
|
||||||
protected readonly canBuyNow = computed(
|
protected readonly canBuyNow = computed(
|
||||||
() => this.hasPurchasableSelection() && this.effectiveAvailability().capabilities.buy_now,
|
() =>
|
||||||
|
this.hasPurchasableSelection() &&
|
||||||
|
allowsCatalogAction(this.effectiveAvailability(), 'buy_now'),
|
||||||
);
|
);
|
||||||
protected readonly restrictionMessage = computed(() =>
|
protected readonly restrictionMessage = computed(() =>
|
||||||
primaryAvailabilityMessage(this.effectiveAvailability()),
|
primaryAvailabilityMessage(this.effectiveAvailability()),
|
||||||
);
|
);
|
||||||
|
protected readonly allows = allowsCatalogAction;
|
||||||
protected readonly descriptionExpanded = signal(false);
|
protected readonly descriptionExpanded = signal(false);
|
||||||
protected readonly descriptionMaxHeight = signal(0);
|
protected readonly descriptionMaxHeight = signal(0);
|
||||||
protected readonly descriptionHasOverflow = signal(false);
|
protected readonly descriptionHasOverflow = signal(false);
|
||||||
@@ -235,8 +243,12 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy {
|
|||||||
this.quantity.set(Math.max(1, maximum));
|
this.quantity.set(Math.max(1, maximum));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: () => {
|
error: (error: HttpErrorResponse) => {
|
||||||
// Keep the last known availability if the silent refresh fails.
|
if (error.status === 404) {
|
||||||
|
this.product.set(null);
|
||||||
|
this.selectedVariant.set(null);
|
||||||
|
this.error.set('Este producto ya no está disponible.');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -308,8 +320,7 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy {
|
|||||||
const variant = this.selectedVariant();
|
const variant = this.selectedVariant();
|
||||||
if (!currentProduct || !this.canAddToCart()) {
|
if (!currentProduct || !this.canAddToCart()) {
|
||||||
this.toastService.danger(
|
this.toastService.danger(
|
||||||
this.effectiveAvailability().restrictions[0]?.message ??
|
this.effectiveAvailability().reasons[0]?.message ?? 'Por favor, selecciona una variante.',
|
||||||
'Por favor, selecciona una variante.',
|
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -342,8 +353,7 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
if (!currentProduct || !this.canBuyNow()) {
|
if (!currentProduct || !this.canBuyNow()) {
|
||||||
this.toastService.danger(
|
this.toastService.danger(
|
||||||
this.effectiveAvailability().restrictions[0]?.message ??
|
this.effectiveAvailability().reasons[0]?.message ?? 'Por favor, selecciona una variante.',
|
||||||
'Por favor, selecciona una variante.',
|
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
[price]="price(item)"
|
[price]="price(item)"
|
||||||
[imageUrl]="loadImages() ? (item.image ?? null) : null"
|
[imageUrl]="loadImages() ? (item.image ?? null) : null"
|
||||||
[unavailableMessage]="availabilityMessage(item)"
|
[unavailableMessage]="availabilityMessage(item)"
|
||||||
[disabled]="loading() || !itemAvailability(item).capabilities.buy_now"
|
[disabled]="loading() || !allows(itemAvailability(item), 'buy_now')"
|
||||||
(buy)="emitTicketBuy(item, $event)"
|
(buy)="emitTicketBuy(item, $event)"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
} from '../../../core/services/catalog/catalog.interface';
|
} from '../../../core/services/catalog/catalog.interface';
|
||||||
import {
|
import {
|
||||||
AVAILABLE_CATALOG_AVAILABILITY,
|
AVAILABLE_CATALOG_AVAILABILITY,
|
||||||
|
allowsCatalogAction,
|
||||||
primaryAvailabilityMessage,
|
primaryAvailabilityMessage,
|
||||||
} from '../../../core/services/catalog/catalog-availability';
|
} from '../../../core/services/catalog/catalog-availability';
|
||||||
import { CarouselComponent } from '../carousel/carousel.component';
|
import { CarouselComponent } from '../carousel/carousel.component';
|
||||||
@@ -78,6 +79,7 @@ export class ProductListComponent {
|
|||||||
readonly buy = output<ProductListBuyEvent>();
|
readonly buy = output<ProductListBuyEvent>();
|
||||||
readonly addToCart = output<ProductListCartEvent>();
|
readonly addToCart = output<ProductListCartEvent>();
|
||||||
readonly pageChange = output<number>();
|
readonly pageChange = output<number>();
|
||||||
|
protected readonly allows = allowsCatalogAction;
|
||||||
|
|
||||||
protected readonly effectiveLayout = computed<ProductListLayout>(() =>
|
protected readonly effectiveLayout = computed<ProductListLayout>(() =>
|
||||||
this.mobile() && this.layout() === 'row' ? 'column_with_cart' : this.layout(),
|
this.mobile() && this.layout() === 'row' ? 'column_with_cart' : this.layout(),
|
||||||
|
|||||||
@@ -21,14 +21,14 @@
|
|||||||
<app-variant-selector
|
<app-variant-selector
|
||||||
class="product-row-card__selectors"
|
class="product-row-card__selectors"
|
||||||
[variants]="variants()"
|
[variants]="variants()"
|
||||||
[disabled]="!availability().capabilities.select_variant"
|
[disabled]="!allows(availability(), 'select_variant')"
|
||||||
[(selectedVariant)]="selectedVariant"
|
[(selectedVariant)]="selectedVariant"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<app-quantity-selector
|
<app-quantity-selector
|
||||||
[(quantity)]="quantity"
|
[(quantity)]="quantity"
|
||||||
[max]="effectiveMaximum()"
|
[max]="effectiveMaximum()"
|
||||||
[disabled]="!effectiveAvailability().capabilities.change_quantity"
|
[disabled]="!allows(effectiveAvailability(), 'change_quantity')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
<div class="product-row-card__btn-wrapper">
|
<div class="product-row-card__btn-wrapper">
|
||||||
<app-button
|
<app-button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
[disabled]="!hasPurchasableSelection() || !effectiveAvailability().capabilities.buy_now"
|
[disabled]="!hasPurchasableSelection() || !allows(effectiveAvailability(), 'buy_now')"
|
||||||
(click)="onBuy()"
|
(click)="onBuy()"
|
||||||
>
|
>
|
||||||
Comprar
|
Comprar
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
[disabled]="
|
[disabled]="
|
||||||
saving() ||
|
saving() ||
|
||||||
!hasPurchasableSelection() ||
|
!hasPurchasableSelection() ||
|
||||||
!effectiveAvailability().capabilities.add_to_cart
|
!allows(effectiveAvailability(), 'add_to_cart')
|
||||||
"
|
"
|
||||||
(click)="onAddToCart()"
|
(click)="onAddToCart()"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ import {
|
|||||||
import { CatalogAvailability } from '../../../core/services/catalog/catalog.interface';
|
import { CatalogAvailability } from '../../../core/services/catalog/catalog.interface';
|
||||||
import {
|
import {
|
||||||
AVAILABLE_CATALOG_AVAILABILITY,
|
AVAILABLE_CATALOG_AVAILABILITY,
|
||||||
|
allowsCatalogAction,
|
||||||
|
combineCatalogAvailability,
|
||||||
|
maximumCatalogQuantity,
|
||||||
primaryAvailabilityMessage,
|
primaryAvailabilityMessage,
|
||||||
} from '../../../core/services/catalog/catalog-availability';
|
} from '../../../core/services/catalog/catalog-availability';
|
||||||
|
|
||||||
@@ -63,24 +66,22 @@ export class ProductRowCardComponent {
|
|||||||
|
|
||||||
return Number.isFinite(variantPrice) ? variantPrice : this.price();
|
return Number.isFinite(variantPrice) ? variantPrice : this.price();
|
||||||
});
|
});
|
||||||
protected readonly effectiveAvailability = computed(
|
protected readonly effectiveAvailability = computed(() =>
|
||||||
() =>
|
combineCatalogAvailability(this.availability(), this.selectedVariantData()?.availability),
|
||||||
this.selectedVariantData()?.availability ??
|
|
||||||
this.availability() ??
|
|
||||||
AVAILABLE_CATALOG_AVAILABILITY,
|
|
||||||
);
|
);
|
||||||
protected readonly hasVariants = computed(() => this.variants().length > 0);
|
protected readonly hasVariants = computed(() => this.variants().length > 0);
|
||||||
protected readonly hasPurchasableSelection = computed(
|
protected readonly hasPurchasableSelection = computed(
|
||||||
() => !this.hasVariants() || this.selectedVariantData() !== undefined,
|
() => !this.hasVariants() || this.selectedVariantData() !== undefined,
|
||||||
);
|
);
|
||||||
protected readonly effectiveMaximum = computed(
|
protected readonly effectiveMaximum = computed(() =>
|
||||||
() => this.effectiveAvailability().maximum_quantity,
|
maximumCatalogQuantity(this.effectiveAvailability()),
|
||||||
);
|
);
|
||||||
protected readonly effectiveUnavailableMessage = computed(() =>
|
protected readonly effectiveUnavailableMessage = computed(() =>
|
||||||
primaryAvailabilityMessage(this.effectiveAvailability()),
|
primaryAvailabilityMessage(this.effectiveAvailability()),
|
||||||
);
|
);
|
||||||
|
|
||||||
readonly formattedPrice = computed(() => this.formatCurrency(this.effectivePrice()));
|
readonly formattedPrice = computed(() => this.formatCurrency(this.effectivePrice()));
|
||||||
|
protected readonly allows = allowsCatalogAction;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
effect(() => {
|
effect(() => {
|
||||||
@@ -96,7 +97,7 @@ export class ProductRowCardComponent {
|
|||||||
if (
|
if (
|
||||||
this.saving() ||
|
this.saving() ||
|
||||||
!this.hasPurchasableSelection() ||
|
!this.hasPurchasableSelection() ||
|
||||||
!this.effectiveAvailability().capabilities.add_to_cart
|
!this.allows(this.effectiveAvailability(), 'add_to_cart')
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -108,7 +109,7 @@ export class ProductRowCardComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected onBuy(): void {
|
protected onBuy(): void {
|
||||||
if (!this.hasPurchasableSelection() || !this.effectiveAvailability().capabilities.buy_now)
|
if (!this.hasPurchasableSelection() || !this.allows(this.effectiveAvailability(), 'buy_now'))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.buy.emit({
|
this.buy.emit({
|
||||||
|
|||||||
@@ -23,7 +23,10 @@ import {
|
|||||||
CatalogVariantSelector,
|
CatalogVariantSelector,
|
||||||
CatalogVariantValue,
|
CatalogVariantValue,
|
||||||
} from '../../../core/services/catalog/catalog.interface';
|
} from '../../../core/services/catalog/catalog.interface';
|
||||||
import { createCatalogAvailability } from '../../../core/services/catalog/catalog-availability';
|
import {
|
||||||
|
allowsCatalogAction,
|
||||||
|
createCatalogAvailability,
|
||||||
|
} from '../../../core/services/catalog/catalog-availability';
|
||||||
import { CatalogService } from '../../../core/services/catalog/catalog.service';
|
import { CatalogService } from '../../../core/services/catalog/catalog.service';
|
||||||
import { ModalService } from '../../../core/services/modal.service';
|
import { ModalService } from '../../../core/services/modal.service';
|
||||||
import { ToastService } from '../../../core/services/toast.service';
|
import { ToastService } from '../../../core/services/toast.service';
|
||||||
@@ -356,7 +359,8 @@ export class ProductTicketSelectorComponent {
|
|||||||
}
|
}
|
||||||
return variants.filter(
|
return variants.filter(
|
||||||
({ id, availability }) =>
|
({ id, availability }) =>
|
||||||
availability?.capabilities.select_variant !== false && !reservedByOtherRows.has(id),
|
(availability === undefined || allowsCatalogAction(availability, 'select_variant')) &&
|
||||||
|
!reservedByOtherRows.has(id),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,7 +371,9 @@ export class ProductTicketSelectorComponent {
|
|||||||
return {
|
return {
|
||||||
id: item.variant.id,
|
id: item.variant.id,
|
||||||
precio: item.variant.precio,
|
precio: item.variant.precio,
|
||||||
availability: createCatalogAvailability(item.variant.stock_tecnico),
|
availability: createCatalogAvailability(
|
||||||
|
item.variant.stock_tecnico === null ? null : item.variant.stock_tecnico + item.cantidad,
|
||||||
|
),
|
||||||
values: item.variant.values,
|
values: item.variant.values,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<app-quantity-selector
|
<app-quantity-selector
|
||||||
[(quantity)]="quantity"
|
[(quantity)]="quantity"
|
||||||
[max]="effectiveMaximum()"
|
[max]="effectiveMaximum()"
|
||||||
[disabled]="!effectiveAvailability().capabilities.change_quantity"
|
[disabled]="!allows(effectiveAvailability(), 'change_quantity')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
@@ -29,14 +29,14 @@
|
|||||||
<app-quantity-selector
|
<app-quantity-selector
|
||||||
[(quantity)]="quantity"
|
[(quantity)]="quantity"
|
||||||
[max]="effectiveMaximum()"
|
[max]="effectiveMaximum()"
|
||||||
[disabled]="!effectiveAvailability().capabilities.change_quantity"
|
[disabled]="!allows(effectiveAvailability(), 'change_quantity')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="product-vertical-with-cart-card__variant-selectors">
|
<div class="product-vertical-with-cart-card__variant-selectors">
|
||||||
<app-variant-selector
|
<app-variant-selector
|
||||||
[variants]="variants()"
|
[variants]="variants()"
|
||||||
[disabled]="!availability().capabilities.select_variant"
|
[disabled]="!allows(availability(), 'select_variant')"
|
||||||
[(selectedVariant)]="selectedVariant"
|
[(selectedVariant)]="selectedVariant"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
<app-button
|
<app-button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
[disabled]="
|
[disabled]="
|
||||||
!effectiveAvailability().capabilities.buy_now ||
|
!allows(effectiveAvailability(), 'buy_now') ||
|
||||||
(hasVariants() && selectedVariant() === null)
|
(hasVariants() && selectedVariant() === null)
|
||||||
"
|
"
|
||||||
(click)="onBuy()"
|
(click)="onBuy()"
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
[disabled]="
|
[disabled]="
|
||||||
saving() ||
|
saving() ||
|
||||||
!effectiveAvailability().capabilities.add_to_cart ||
|
!allows(effectiveAvailability(), 'add_to_cart') ||
|
||||||
(hasVariants() && selectedVariant() === null)
|
(hasVariants() && selectedVariant() === null)
|
||||||
"
|
"
|
||||||
(click)="onAddToCart()"
|
(click)="onAddToCart()"
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ import {
|
|||||||
import { CatalogAvailability } from '../../../core/services/catalog/catalog.interface';
|
import { CatalogAvailability } from '../../../core/services/catalog/catalog.interface';
|
||||||
import {
|
import {
|
||||||
AVAILABLE_CATALOG_AVAILABILITY,
|
AVAILABLE_CATALOG_AVAILABILITY,
|
||||||
|
allowsCatalogAction,
|
||||||
|
combineCatalogAvailability,
|
||||||
|
maximumCatalogQuantity,
|
||||||
primaryAvailabilityMessage,
|
primaryAvailabilityMessage,
|
||||||
} from '../../../core/services/catalog/catalog-availability';
|
} from '../../../core/services/catalog/catalog-availability';
|
||||||
|
|
||||||
@@ -61,18 +64,16 @@ export class ProductVerticalWithCartCardComponent {
|
|||||||
});
|
});
|
||||||
protected readonly formattedPrice = computed(() => this.formatCurrency(this.effectivePrice()));
|
protected readonly formattedPrice = computed(() => this.formatCurrency(this.effectivePrice()));
|
||||||
protected readonly hasVariants = computed(() => this.variants().length > 0);
|
protected readonly hasVariants = computed(() => this.variants().length > 0);
|
||||||
protected readonly effectiveAvailability = computed(
|
protected readonly effectiveAvailability = computed(() =>
|
||||||
() =>
|
combineCatalogAvailability(this.availability(), this.selectedVariantData()?.availability),
|
||||||
this.selectedVariantData()?.availability ??
|
|
||||||
this.availability() ??
|
|
||||||
AVAILABLE_CATALOG_AVAILABILITY,
|
|
||||||
);
|
);
|
||||||
protected readonly effectiveMaximum = computed(
|
protected readonly effectiveMaximum = computed(() =>
|
||||||
() => this.effectiveAvailability().maximum_quantity,
|
maximumCatalogQuantity(this.effectiveAvailability()),
|
||||||
);
|
);
|
||||||
protected readonly effectiveUnavailableMessage = computed(() =>
|
protected readonly effectiveUnavailableMessage = computed(() =>
|
||||||
primaryAvailabilityMessage(this.effectiveAvailability()),
|
primaryAvailabilityMessage(this.effectiveAvailability()),
|
||||||
);
|
);
|
||||||
|
protected readonly allows = allowsCatalogAction;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
effect(() => {
|
effect(() => {
|
||||||
@@ -85,7 +86,7 @@ export class ProductVerticalWithCartCardComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected onAddToCart(): void {
|
protected onAddToCart(): void {
|
||||||
if (this.saving() || !this.effectiveAvailability().capabilities.add_to_cart) {
|
if (this.saving() || !this.allows(this.effectiveAvailability(), 'add_to_cart')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +94,7 @@ export class ProductVerticalWithCartCardComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected onBuy(): void {
|
protected onBuy(): void {
|
||||||
if (!this.effectiveAvailability().capabilities.buy_now) return;
|
if (!this.allows(this.effectiveAvailability(), 'buy_now')) return;
|
||||||
|
|
||||||
this.buy.emit({ quantity: this.quantity(), variant: this.selectedVariant() });
|
this.buy.emit({ quantity: this.quantity(), variant: this.selectedVariant() });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
untracked,
|
untracked,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { CatalogAvailability } from '../../../core/services/catalog/catalog.interface';
|
||||||
|
import { allowsCatalogAction } from '../../../core/services/catalog/catalog-availability';
|
||||||
|
|
||||||
export interface VariantAttributeOption {
|
export interface VariantAttributeOption {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -22,12 +24,7 @@ export type VariantAttributeValue = VariantAttributeScalar | VariantAttributeSca
|
|||||||
export interface VariantSelectorVariant {
|
export interface VariantSelectorVariant {
|
||||||
id: unknown;
|
id: unknown;
|
||||||
values: Record<string, VariantAttributeValue>;
|
values: Record<string, VariantAttributeValue>;
|
||||||
availability?: {
|
availability?: CatalogAvailability;
|
||||||
capabilities: {
|
|
||||||
display: boolean;
|
|
||||||
select_variant: boolean;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VariantSelectorSelectionChange {
|
export interface VariantSelectorSelectionChange {
|
||||||
@@ -208,7 +205,9 @@ export class VariantSelectorComponent {
|
|||||||
|
|
||||||
private getSelectableVariants(): VariantSelectorVariant[] {
|
private getSelectableVariants(): VariantSelectorVariant[] {
|
||||||
return this.variants().filter(
|
return this.variants().filter(
|
||||||
(variant) => variant.availability?.capabilities.select_variant !== false,
|
(variant) =>
|
||||||
|
variant.availability === undefined ||
|
||||||
|
allowsCatalogAction(variant.availability, 'select_variant'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user