feat(catalog): consume availability capabilities
This commit is contained in:
39
src/app/core/services/catalog/catalog-availability.ts
Normal file
39
src/app/core/services/catalog/catalog-availability.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { CatalogAvailability } from './catalog.interface';
|
||||||
|
|
||||||
|
export const AVAILABLE_CATALOG_AVAILABILITY: CatalogAvailability = {
|
||||||
|
maximum_quantity: null,
|
||||||
|
restrictions: [],
|
||||||
|
capabilities: {
|
||||||
|
select_variant: true,
|
||||||
|
change_quantity: true,
|
||||||
|
add_to_cart: true,
|
||||||
|
buy_now: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createCatalogAvailability(maximumQuantity: number | null): CatalogAvailability {
|
||||||
|
const unavailable = maximumQuantity === 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
maximum_quantity: maximumQuantity,
|
||||||
|
restrictions: unavailable
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
code: 'out_of_stock',
|
||||||
|
message: 'Este producto no tiene stock disponible.',
|
||||||
|
scope: 'product',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
capabilities: {
|
||||||
|
select_variant: !unavailable,
|
||||||
|
change_quantity: !unavailable,
|
||||||
|
add_to_cart: !unavailable,
|
||||||
|
buy_now: !unavailable,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function primaryAvailabilityMessage(availability: CatalogAvailability): string | null {
|
||||||
|
return availability.restrictions[0]?.message ?? null;
|
||||||
|
}
|
||||||
@@ -49,6 +49,25 @@ export interface ProductAttribute {
|
|||||||
|
|
||||||
export type InventoryPolicy = 'tracked' | 'unlimited';
|
export type InventoryPolicy = 'tracked' | 'unlimited';
|
||||||
|
|
||||||
|
export interface CatalogRestriction {
|
||||||
|
code: 'out_of_stock' | 'user_quota_reached' | string;
|
||||||
|
message: string;
|
||||||
|
scope: 'product' | 'variant' | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CatalogCapabilities {
|
||||||
|
select_variant: boolean;
|
||||||
|
change_quantity: boolean;
|
||||||
|
add_to_cart: boolean;
|
||||||
|
buy_now: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CatalogAvailability {
|
||||||
|
maximum_quantity: number | null;
|
||||||
|
restrictions: CatalogRestriction[];
|
||||||
|
capabilities: CatalogCapabilities;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CatalogVariantOption {
|
export interface CatalogVariantOption {
|
||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -66,8 +85,7 @@ export interface CatalogItemVariant {
|
|||||||
event_date_id?: number | null;
|
event_date_id?: number | null;
|
||||||
event_date_ids?: number[];
|
event_date_ids?: number[];
|
||||||
event_dates?: string[];
|
event_dates?: string[];
|
||||||
maximum_addable_quantity?: number | null;
|
availability: CatalogAvailability;
|
||||||
unavailable_message?: string | null;
|
|
||||||
minimum_use_date?: string | null;
|
minimum_use_date?: string | null;
|
||||||
maximum_use_date?: string | null;
|
maximum_use_date?: string | null;
|
||||||
effective_minimum_use_date?: string | null;
|
effective_minimum_use_date?: string | null;
|
||||||
@@ -99,7 +117,7 @@ export interface CatalogItemDetail {
|
|||||||
attributes: ProductAttribute[];
|
attributes: ProductAttribute[];
|
||||||
variants: CatalogItemVariant[];
|
variants: CatalogItemVariant[];
|
||||||
selected_variant?: SelectedCatalogItemVariant;
|
selected_variant?: SelectedCatalogItemVariant;
|
||||||
maximum_addable_quantity?: number | null;
|
availability: CatalogAvailability;
|
||||||
images?: string[];
|
images?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,8 +132,7 @@ export interface CatalogFeaturedItemVariant {
|
|||||||
id: number;
|
id: number;
|
||||||
descripcion?: string | null;
|
descripcion?: string | null;
|
||||||
precio?: string;
|
precio?: string;
|
||||||
maximum_addable_quantity?: number | null;
|
availability: CatalogAvailability;
|
||||||
unavailable_message?: string | null;
|
|
||||||
values: Record<string, CatalogVariantValue>;
|
values: Record<string, CatalogVariantValue>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,8 +164,7 @@ export interface CatalogFeaturedItem {
|
|||||||
descripcion?: string | null;
|
descripcion?: string | null;
|
||||||
precio: number | string;
|
precio: number | string;
|
||||||
image?: string | null;
|
image?: string | null;
|
||||||
maximum_addable_quantity?: number | null;
|
availability: CatalogAvailability;
|
||||||
unavailable_message?: string | null;
|
|
||||||
variants?: CatalogFeaturedItemVariant[];
|
variants?: CatalogFeaturedItemVariant[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { StoreSectionComponent } from '../../../../shared/components/store-secti
|
|||||||
import { ModalService } from '../../../../core/services/modal.service';
|
import { ModalService } from '../../../../core/services/modal.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 { createCatalogAvailability } from '../../../../core/services/catalog/catalog-availability';
|
||||||
import { StepperComponent } from '../../../../shared/components/stepper/stepper.component';
|
import { StepperComponent } from '../../../../shared/components/stepper/stepper.component';
|
||||||
import { StepComponent } from '../../../../shared/components/stepper/step.component';
|
import { StepComponent } from '../../../../shared/components/stepper/step.component';
|
||||||
import { CarouselComponent } from '../../../../shared/components/carousel/carousel.component';
|
import { CarouselComponent } from '../../../../shared/components/carousel/carousel.component';
|
||||||
@@ -232,7 +233,7 @@ export class ReutilizablesTestPageComponent {
|
|||||||
{
|
{
|
||||||
id: 1001,
|
id: 1001,
|
||||||
precio: 250000,
|
precio: 250000,
|
||||||
maximum_addable_quantity: 1,
|
availability: createCatalogAvailability(1),
|
||||||
values: {
|
values: {
|
||||||
tipo: ticketOption('vip_lunch', 'VIP + Lunch'),
|
tipo: ticketOption('vip_lunch', 'VIP + Lunch'),
|
||||||
sector: ticketOption('a', 'Sector A'),
|
sector: ticketOption('a', 'Sector A'),
|
||||||
@@ -243,7 +244,7 @@ export class ReutilizablesTestPageComponent {
|
|||||||
{
|
{
|
||||||
id: 1002,
|
id: 1002,
|
||||||
precio: 250000,
|
precio: 250000,
|
||||||
maximum_addable_quantity: 1,
|
availability: createCatalogAvailability(1),
|
||||||
values: {
|
values: {
|
||||||
tipo: ticketOption('vip_lunch', 'VIP + Lunch'),
|
tipo: ticketOption('vip_lunch', 'VIP + Lunch'),
|
||||||
sector: ticketOption('a', 'Sector A'),
|
sector: ticketOption('a', 'Sector A'),
|
||||||
@@ -254,7 +255,7 @@ export class ReutilizablesTestPageComponent {
|
|||||||
{
|
{
|
||||||
id: 1003,
|
id: 1003,
|
||||||
precio: 250000,
|
precio: 250000,
|
||||||
maximum_addable_quantity: 1,
|
availability: createCatalogAvailability(1),
|
||||||
values: {
|
values: {
|
||||||
tipo: ticketOption('vip_lunch', 'VIP + Lunch'),
|
tipo: ticketOption('vip_lunch', 'VIP + Lunch'),
|
||||||
sector: ticketOption('c', 'Sector C'),
|
sector: ticketOption('c', 'Sector C'),
|
||||||
@@ -265,7 +266,7 @@ export class ReutilizablesTestPageComponent {
|
|||||||
{
|
{
|
||||||
id: 1004,
|
id: 1004,
|
||||||
precio: 200000,
|
precio: 200000,
|
||||||
maximum_addable_quantity: 1,
|
availability: createCatalogAvailability(1),
|
||||||
values: {
|
values: {
|
||||||
tipo: ticketOption('vip_lunch', 'VIP + Lunch'),
|
tipo: ticketOption('vip_lunch', 'VIP + Lunch'),
|
||||||
sector: ticketOption('a', 'Sector A'),
|
sector: ticketOption('a', 'Sector A'),
|
||||||
@@ -276,7 +277,7 @@ export class ReutilizablesTestPageComponent {
|
|||||||
{
|
{
|
||||||
id: 1005,
|
id: 1005,
|
||||||
precio: 200000,
|
precio: 200000,
|
||||||
maximum_addable_quantity: 0,
|
availability: createCatalogAvailability(0),
|
||||||
values: {
|
values: {
|
||||||
tipo: ticketOption('vip_lunch', 'VIP + Lunch'),
|
tipo: ticketOption('vip_lunch', 'VIP + Lunch'),
|
||||||
sector: ticketOption('a', 'Sector A'),
|
sector: ticketOption('a', 'Sector A'),
|
||||||
@@ -287,7 +288,7 @@ export class ReutilizablesTestPageComponent {
|
|||||||
{
|
{
|
||||||
id: 1006,
|
id: 1006,
|
||||||
precio: 100000,
|
precio: 100000,
|
||||||
maximum_addable_quantity: 1,
|
availability: createCatalogAvailability(1),
|
||||||
values: {
|
values: {
|
||||||
tipo: ticketOption('general', 'General'),
|
tipo: ticketOption('general', 'General'),
|
||||||
sector: ticketOption('b', 'Sector B'),
|
sector: ticketOption('b', 'Sector B'),
|
||||||
@@ -298,7 +299,7 @@ export class ReutilizablesTestPageComponent {
|
|||||||
{
|
{
|
||||||
id: 1007,
|
id: 1007,
|
||||||
precio: 100000,
|
precio: 100000,
|
||||||
maximum_addable_quantity: 1,
|
availability: createCatalogAvailability(1),
|
||||||
values: {
|
values: {
|
||||||
tipo: ticketOption('general', 'General'),
|
tipo: ticketOption('general', 'General'),
|
||||||
sector: ticketOption('b', 'Sector B'),
|
sector: ticketOption('b', 'Sector B'),
|
||||||
@@ -309,7 +310,7 @@ export class ReutilizablesTestPageComponent {
|
|||||||
{
|
{
|
||||||
id: 1008,
|
id: 1008,
|
||||||
precio: 90000,
|
precio: 90000,
|
||||||
maximum_addable_quantity: 1,
|
availability: createCatalogAvailability(1),
|
||||||
values: {
|
values: {
|
||||||
tipo: ticketOption('general', 'General'),
|
tipo: ticketOption('general', 'General'),
|
||||||
sector: ticketOption('d', 'Sector D'),
|
sector: ticketOption('d', 'Sector D'),
|
||||||
@@ -320,7 +321,7 @@ export class ReutilizablesTestPageComponent {
|
|||||||
{
|
{
|
||||||
id: 1009,
|
id: 1009,
|
||||||
precio: 65000,
|
precio: 65000,
|
||||||
maximum_addable_quantity: 1,
|
availability: createCatalogAvailability(1),
|
||||||
values: {
|
values: {
|
||||||
tipo: ticketOption('general', 'General'),
|
tipo: ticketOption('general', 'General'),
|
||||||
sector: ticketOption('d', 'Sector D'),
|
sector: ticketOption('d', 'Sector D'),
|
||||||
@@ -331,7 +332,7 @@ export class ReutilizablesTestPageComponent {
|
|||||||
{
|
{
|
||||||
id: 1010,
|
id: 1010,
|
||||||
precio: 40000,
|
precio: 40000,
|
||||||
maximum_addable_quantity: 1,
|
availability: createCatalogAvailability(1),
|
||||||
values: {
|
values: {
|
||||||
tipo: ticketOption('general', 'General'),
|
tipo: ticketOption('general', 'General'),
|
||||||
sector: ticketOption('d', 'Sector D'),
|
sector: ticketOption('d', 'Sector D'),
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
[attr.aria-label]="option.label"
|
[attr.aria-label]="option.label"
|
||||||
[attr.aria-pressed]="hasSelectedOption(attribute, option)"
|
[attr.aria-pressed]="hasSelectedOption(attribute, option)"
|
||||||
[title]="option.label"
|
[title]="option.label"
|
||||||
[disabled]="!availableOptions()[attribute.codigo][option.id]"
|
[disabled]="disabled() || !availableOptions()[attribute.codigo][option.id]"
|
||||||
(click)="selectAttributeOption(attribute, option)"
|
(click)="selectAttributeOption(attribute, option)"
|
||||||
>
|
>
|
||||||
<span class="visually-hidden">{{ option.label }}</span>
|
<span class="visually-hidden">{{ option.label }}</span>
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
!availableOptions()[attribute.codigo][option.id]
|
!availableOptions()[attribute.codigo][option.id]
|
||||||
"
|
"
|
||||||
[attr.aria-pressed]="hasSelectedOption(attribute, option)"
|
[attr.aria-pressed]="hasSelectedOption(attribute, option)"
|
||||||
[disabled]="!availableOptions()[attribute.codigo][option.id]"
|
[disabled]="disabled() || !availableOptions()[attribute.codigo][option.id]"
|
||||||
(click)="selectAttributeOption(attribute, option)"
|
(click)="selectAttributeOption(attribute, option)"
|
||||||
>
|
>
|
||||||
{{ option.label }}
|
{{ option.label }}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export class ProductAttributeSelectorComponent {
|
|||||||
public variants = input<CatalogItemVariant[]>([]);
|
public variants = input<CatalogItemVariant[]>([]);
|
||||||
public selectedVariant = input<CatalogItemVariant | null>(null);
|
public selectedVariant = input<CatalogItemVariant | null>(null);
|
||||||
public inventoryPolicy = input.required<InventoryPolicy>();
|
public inventoryPolicy = input.required<InventoryPolicy>();
|
||||||
|
public disabled = input(false);
|
||||||
|
|
||||||
public variantChange = output<CatalogItemVariant | null>();
|
public variantChange = output<CatalogItemVariant | null>();
|
||||||
|
|
||||||
@@ -51,22 +52,15 @@ export class ProductAttributeSelectorComponent {
|
|||||||
const optionNormalized = this.normalizeText(option.value || option.label);
|
const optionNormalized = this.normalizeText(option.value || option.label);
|
||||||
const selectedForAttribute = selections[attribute.codigo] ?? [];
|
const selectedForAttribute = selections[attribute.codigo] ?? [];
|
||||||
|
|
||||||
if (
|
|
||||||
!attribute.allow_multi_select &&
|
|
||||||
selectedForAttribute.length >= 1 &&
|
|
||||||
!selectedForAttribute.includes(option.id)
|
|
||||||
) {
|
|
||||||
availability[attribute.codigo][option.id] = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAvailable = variants.some((variant) => {
|
const isAvailable = variants.some((variant) => {
|
||||||
if (!this.isVariantAvailable(variant)) return false;
|
if (!this.isVariantAvailable(variant)) return false;
|
||||||
|
|
||||||
const variantAttrValues = this.getVariantAttributeValues(attribute, variant.values);
|
const variantAttrValues = this.getVariantAttributeValues(attribute, variant.values);
|
||||||
const desiredOptionIds = selectedForAttribute.includes(option.id)
|
const desiredOptionIds = attribute.allow_multi_select
|
||||||
? selectedForAttribute
|
? selectedForAttribute.includes(option.id)
|
||||||
: [...selectedForAttribute, option.id];
|
? selectedForAttribute
|
||||||
|
: [...selectedForAttribute, option.id]
|
||||||
|
: [option.id];
|
||||||
const desiredValues = desiredOptionIds
|
const desiredValues = desiredOptionIds
|
||||||
.map((id) => attribute.options.find((candidate) => candidate.id === id))
|
.map((id) => attribute.options.find((candidate) => candidate.id === id))
|
||||||
.filter((candidate): candidate is ProductAttributeOption => candidate !== undefined)
|
.filter((candidate): candidate is ProductAttributeOption => candidate !== undefined)
|
||||||
@@ -137,6 +131,8 @@ export class ProductAttributeSelectorComponent {
|
|||||||
attribute: ProductAttribute,
|
attribute: ProductAttribute,
|
||||||
option: ProductAttributeOption,
|
option: ProductAttributeOption,
|
||||||
): void {
|
): void {
|
||||||
|
if (this.disabled()) return;
|
||||||
|
|
||||||
this.selectedAttributeOptions.update((current) => {
|
this.selectedAttributeOptions.update((current) => {
|
||||||
const selected = current[attribute.codigo] ?? [];
|
const selected = current[attribute.codigo] ?? [];
|
||||||
const isMultiple = attribute.allow_multi_select ?? false;
|
const isMultiple = attribute.allow_multi_select ?? false;
|
||||||
@@ -186,7 +182,15 @@ export class ProductAttributeSelectorComponent {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultValues = this.getVariantAttributeValues(attribute, variant.values);
|
const defaultValues =
|
||||||
|
attribute.type === 'event_date'
|
||||||
|
? (
|
||||||
|
variant.event_date_ids ??
|
||||||
|
(variant.event_date_id === null || variant.event_date_id === undefined
|
||||||
|
? []
|
||||||
|
: [variant.event_date_id])
|
||||||
|
).map(String)
|
||||||
|
: this.getVariantAttributeValues(attribute, variant.values);
|
||||||
if (defaultValues.length === 0) {
|
if (defaultValues.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -215,6 +219,17 @@ export class ProductAttributeSelectorComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (attribute.type === 'event_date') {
|
||||||
|
const variant = this.variants().find((candidate) => candidate.values === variantAttributes);
|
||||||
|
const eventDateIds =
|
||||||
|
variant?.event_date_ids ??
|
||||||
|
(variant?.event_date_id === null || variant?.event_date_id === undefined
|
||||||
|
? []
|
||||||
|
: [variant.event_date_id]);
|
||||||
|
|
||||||
|
return eventDateIds.map(String);
|
||||||
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,7 +242,7 @@ export class ProductAttributeSelectorComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private isVariantAvailable(variant: CatalogItemVariant): boolean {
|
private isVariantAvailable(variant: CatalogItemVariant): boolean {
|
||||||
return variant.maximum_addable_quantity !== 0;
|
return variant.availability.capabilities.select_variant;
|
||||||
}
|
}
|
||||||
|
|
||||||
private findFirstHexValue(value: unknown): string | null {
|
private findFirstHexValue(value: unknown): string | null {
|
||||||
|
|||||||
@@ -44,6 +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"
|
||||||
(variantChange)="onVariantChange($event)"
|
(variantChange)="onVariantChange($event)"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
@@ -53,14 +54,22 @@
|
|||||||
|
|
||||||
<section class="product-detail__section">
|
<section class="product-detail__section">
|
||||||
<div class="product-detail__purchase">
|
<div class="product-detail__purchase">
|
||||||
<app-quantity-selector [(quantity)]="quantity" [max]="selectedVariantMax()" />
|
@if (restrictionMessage(); as message) {
|
||||||
|
<p class="mb-0 text-danger" role="status">{{ message }}</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
<app-quantity-selector
|
||||||
|
[(quantity)]="quantity"
|
||||||
|
[max]="selectedVariantMax()"
|
||||||
|
[disabled]="!effectiveAvailability().capabilities.change_quantity"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="product-detail__actions">
|
<div class="product-detail__actions">
|
||||||
<app-button
|
<app-button
|
||||||
class="product-detail__cta"
|
class="product-detail__cta"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
type="button"
|
type="button"
|
||||||
[disabled]="!selectedVariantAvailable() || variantLoading() || addingToCart()"
|
[disabled]="!canAddToCart() || variantLoading() || addingToCart()"
|
||||||
(click)="addToCart()"
|
(click)="addToCart()"
|
||||||
>
|
>
|
||||||
@if (addingToCart()) {
|
@if (addingToCart()) {
|
||||||
@@ -75,9 +84,7 @@
|
|||||||
<app-button
|
<app-button
|
||||||
class="product-detail__cta"
|
class="product-detail__cta"
|
||||||
type="button"
|
type="button"
|
||||||
[disabled]="
|
[disabled]="!canBuyNow() || variantLoading() || creatingDirectPurchase()"
|
||||||
!selectedVariantAvailable() || variantLoading() || creatingDirectPurchase()
|
|
||||||
"
|
|
||||||
(click)="buyNow()"
|
(click)="buyNow()"
|
||||||
>
|
>
|
||||||
@if (variantLoading() || creatingDirectPurchase()) {
|
@if (variantLoading() || creatingDirectPurchase()) {
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ import { ProductDetailResolvedData } from './product-detail-page.resolver';
|
|||||||
import { CheckoutService } from '../../../../core/services/checkout.service';
|
import { CheckoutService } from '../../../../core/services/checkout.service';
|
||||||
import { TenantService } from '../../../../core/services/tenant.service';
|
import { TenantService } from '../../../../core/services/tenant.service';
|
||||||
import { AuthService } from '../../../../core/services/auth/auth.service';
|
import { AuthService } from '../../../../core/services/auth/auth.service';
|
||||||
|
import {
|
||||||
|
AVAILABLE_CATALOG_AVAILABILITY,
|
||||||
|
primaryAvailabilityMessage,
|
||||||
|
} from '../../../../core/services/catalog/catalog-availability';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-product-detail-page',
|
selector: 'app-product-detail-page',
|
||||||
@@ -96,29 +100,36 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy {
|
|||||||
() => this.selectedVariant()?.precio ?? this.product()?.precio,
|
() => this.selectedVariant()?.precio ?? this.product()?.precio,
|
||||||
);
|
);
|
||||||
protected readonly quantity = signal(1);
|
protected readonly quantity = signal(1);
|
||||||
|
protected readonly effectiveAvailability = computed(() => {
|
||||||
|
const prod = this.product();
|
||||||
|
const variant = this.selectedVariant();
|
||||||
|
if (!prod) return AVAILABLE_CATALOG_AVAILABILITY;
|
||||||
|
|
||||||
|
return variant?.availability ?? prod.availability;
|
||||||
|
});
|
||||||
protected readonly selectedVariantMax = computed<number | null>(() => {
|
protected readonly selectedVariantMax = computed<number | null>(() => {
|
||||||
const prod = this.product();
|
const prod = this.product();
|
||||||
const variant = this.selectedVariant();
|
const variant = this.selectedVariant();
|
||||||
if (!prod) return 0;
|
if (!prod) return 0;
|
||||||
|
if (!variant && prod.variants.length > 0) return 0;
|
||||||
|
|
||||||
return variant
|
return this.effectiveAvailability().maximum_quantity;
|
||||||
? (variant.maximum_addable_quantity ?? null)
|
|
||||||
: prod.variants.length === 0
|
|
||||||
? (prod.maximum_addable_quantity ?? null)
|
|
||||||
: 0;
|
|
||||||
});
|
});
|
||||||
protected readonly selectedVariantAvailable = computed(() => {
|
protected readonly hasPurchasableSelection = computed(() => {
|
||||||
const prod = this.product();
|
const prod = this.product();
|
||||||
if (!prod) return false;
|
if (!prod) return false;
|
||||||
if (this.selectedVariantMax() === 0) return false;
|
|
||||||
|
|
||||||
const variant = this.selectedVariant();
|
return prod.variants.length === 0 || this.selectedVariant() !== null;
|
||||||
if (variant) return this.isVariantAvailable(variant);
|
|
||||||
if (prod.purpose === 'entry') return false;
|
|
||||||
if (prod.variants.length > 0) return false;
|
|
||||||
|
|
||||||
return this.selectedVariantMax() !== 0;
|
|
||||||
});
|
});
|
||||||
|
protected readonly canAddToCart = computed(
|
||||||
|
() => this.hasPurchasableSelection() && this.effectiveAvailability().capabilities.add_to_cart,
|
||||||
|
);
|
||||||
|
protected readonly canBuyNow = computed(
|
||||||
|
() => this.hasPurchasableSelection() && this.effectiveAvailability().capabilities.buy_now,
|
||||||
|
);
|
||||||
|
protected readonly restrictionMessage = computed(() =>
|
||||||
|
primaryAvailabilityMessage(this.effectiveAvailability()),
|
||||||
|
);
|
||||||
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);
|
||||||
@@ -295,8 +306,11 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy {
|
|||||||
protected addToCart(): void {
|
protected addToCart(): void {
|
||||||
const currentProduct = this.product();
|
const currentProduct = this.product();
|
||||||
const variant = this.selectedVariant();
|
const variant = this.selectedVariant();
|
||||||
if (!currentProduct || !this.selectedVariantAvailable()) {
|
if (!currentProduct || !this.canAddToCart()) {
|
||||||
this.toastService.danger('Por favor, selecciona una variante.');
|
this.toastService.danger(
|
||||||
|
this.effectiveAvailability().restrictions[0]?.message ??
|
||||||
|
'Por favor, selecciona una variante.',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,8 +340,11 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!currentProduct || !this.selectedVariantAvailable()) {
|
if (!currentProduct || !this.canBuyNow()) {
|
||||||
this.toastService.danger('Por favor, selecciona una variante.');
|
this.toastService.danger(
|
||||||
|
this.effectiveAvailability().restrictions[0]?.message ??
|
||||||
|
'Por favor, selecciona una variante.',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,10 +390,6 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private isVariantAvailable(variant: CatalogItemVariant): boolean {
|
|
||||||
return variant.maximum_addable_quantity !== 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected toggleDescription(): void {
|
protected toggleDescription(): void {
|
||||||
this.descriptionExpanded.update((current) => !current);
|
this.descriptionExpanded.update((current) => !current);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,7 @@
|
|||||||
[title]="item.nombre"
|
[title]="item.nombre"
|
||||||
[description]="item.descripcion ?? ''"
|
[description]="item.descripcion ?? ''"
|
||||||
[price]="price(item)"
|
[price]="price(item)"
|
||||||
[maximumAddableQuantity]="item.maximum_addable_quantity ?? null"
|
[availability]="itemAvailability(item)"
|
||||||
[unavailableMessage]="item.unavailable_message ?? null"
|
|
||||||
[variants]="item.variants ?? []"
|
[variants]="item.variants ?? []"
|
||||||
[saving]="savingProductIds().has(item.id)"
|
[saving]="savingProductIds().has(item.id)"
|
||||||
(buy)="emitRowBuy(item, $event)"
|
(buy)="emitRowBuy(item, $event)"
|
||||||
@@ -25,8 +24,7 @@
|
|||||||
[title]="item.nombre"
|
[title]="item.nombre"
|
||||||
[description]="item.descripcion ?? ''"
|
[description]="item.descripcion ?? ''"
|
||||||
[price]="price(item)"
|
[price]="price(item)"
|
||||||
[maximumAddableQuantity]="item.maximum_addable_quantity ?? null"
|
[availability]="itemAvailability(item)"
|
||||||
[unavailableMessage]="item.unavailable_message ?? null"
|
|
||||||
[variants]="item.variants ?? []"
|
[variants]="item.variants ?? []"
|
||||||
[saving]="savingProductIds().has(item.id)"
|
[saving]="savingProductIds().has(item.id)"
|
||||||
(buy)="emitColumnBuy(item, $event)"
|
(buy)="emitColumnBuy(item, $event)"
|
||||||
@@ -40,8 +38,8 @@
|
|||||||
[description]="item.descripcion ?? ''"
|
[description]="item.descripcion ?? ''"
|
||||||
[price]="price(item)"
|
[price]="price(item)"
|
||||||
[imageUrl]="loadImages() ? (item.image ?? null) : null"
|
[imageUrl]="loadImages() ? (item.image ?? null) : null"
|
||||||
[unavailableMessage]="item.unavailable_message ?? null"
|
[unavailableMessage]="availabilityMessage(item)"
|
||||||
[disabled]="loading() || !!item.unavailable_message"
|
[disabled]="loading() || !itemAvailability(item).capabilities.buy_now"
|
||||||
(buy)="emitTicketBuy(item, $event)"
|
(buy)="emitTicketBuy(item, $event)"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -50,7 +48,7 @@
|
|||||||
[imageUrl]="loadImages() ? (item.image ?? null) : null"
|
[imageUrl]="loadImages() ? (item.image ?? null) : null"
|
||||||
[title]="item.nombre"
|
[title]="item.nombre"
|
||||||
[originalPrice]="price(item)"
|
[originalPrice]="price(item)"
|
||||||
[unavailableMessage]="item.unavailable_message ?? null"
|
[unavailableMessage]="availabilityMessage(item)"
|
||||||
[imagePriority]="loadImages() && index < 4"
|
[imagePriority]="loadImages() && index < 4"
|
||||||
(buy)="emitProductDetailBuy(item)"
|
(buy)="emitProductDetailBuy(item)"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ import {
|
|||||||
CatalogGroupLayout,
|
CatalogGroupLayout,
|
||||||
CatalogProductLayout,
|
CatalogProductLayout,
|
||||||
} from '../../../core/services/catalog/catalog.interface';
|
} from '../../../core/services/catalog/catalog.interface';
|
||||||
|
import {
|
||||||
|
AVAILABLE_CATALOG_AVAILABILITY,
|
||||||
|
primaryAvailabilityMessage,
|
||||||
|
} from '../../../core/services/catalog/catalog-availability';
|
||||||
import { CarouselComponent } from '../carousel/carousel.component';
|
import { CarouselComponent } from '../carousel/carousel.component';
|
||||||
import { PaginatorComponent } from '../paginator/paginator.component';
|
import { PaginatorComponent } from '../paginator/paginator.component';
|
||||||
import { ProductColumnWithImageComponent } from '../product-column-with-image/product-column-with-image.component';
|
import { ProductColumnWithImageComponent } from '../product-column-with-image/product-column-with-image.component';
|
||||||
@@ -108,6 +112,14 @@ export class ProductListComponent {
|
|||||||
return Number.isFinite(price) ? price : 0;
|
return Number.isFinite(price) ? price : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected availabilityMessage(item: ProductListItem): string | null {
|
||||||
|
return primaryAvailabilityMessage(this.itemAvailability(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected itemAvailability(item: ProductListItem) {
|
||||||
|
return item.availability ?? AVAILABLE_CATALOG_AVAILABILITY;
|
||||||
|
}
|
||||||
|
|
||||||
protected emitRowCart(
|
protected emitRowCart(
|
||||||
product: ProductListItem,
|
product: ProductListItem,
|
||||||
event: { quantity: number; variant: unknown },
|
event: { quantity: number; variant: unknown },
|
||||||
|
|||||||
@@ -21,13 +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"
|
||||||
[(selectedVariant)]="selectedVariant"
|
[(selectedVariant)]="selectedVariant"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<app-quantity-selector
|
<app-quantity-selector
|
||||||
[(quantity)]="quantity"
|
[(quantity)]="quantity"
|
||||||
[max]="effectiveMaximum()"
|
[max]="effectiveMaximum()"
|
||||||
[disabled]="unavailable()"
|
[disabled]="!effectiveAvailability().capabilities.change_quantity"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -38,14 +39,22 @@
|
|||||||
|
|
||||||
<div class="product-row-card__buttons">
|
<div class="product-row-card__buttons">
|
||||||
<div class="product-row-card__btn-wrapper">
|
<div class="product-row-card__btn-wrapper">
|
||||||
<app-button variant="primary" [disabled]="unavailable()" (click)="onBuy()">
|
<app-button
|
||||||
|
variant="primary"
|
||||||
|
[disabled]="!hasPurchasableSelection() || !effectiveAvailability().capabilities.buy_now"
|
||||||
|
(click)="onBuy()"
|
||||||
|
>
|
||||||
Comprar
|
Comprar
|
||||||
</app-button>
|
</app-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="product-row-card__btn-wrapper">
|
<div class="product-row-card__btn-wrapper">
|
||||||
<app-button
|
<app-button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
[disabled]="saving() || unavailable()"
|
[disabled]="
|
||||||
|
saving() ||
|
||||||
|
!hasPurchasableSelection() ||
|
||||||
|
!effectiveAvailability().capabilities.add_to_cart
|
||||||
|
"
|
||||||
(click)="onAddToCart()"
|
(click)="onAddToCart()"
|
||||||
>
|
>
|
||||||
{{ saving() ? 'Guardando' : 'Agregar al carrito' }}
|
{{ saving() ? 'Guardando' : 'Agregar al carrito' }}
|
||||||
|
|||||||
@@ -14,13 +14,17 @@ import {
|
|||||||
VariantSelectorComponent,
|
VariantSelectorComponent,
|
||||||
VariantSelectorVariant,
|
VariantSelectorVariant,
|
||||||
} from '../variant-selector/variant-selector.component';
|
} from '../variant-selector/variant-selector.component';
|
||||||
|
import { CatalogAvailability } from '../../../core/services/catalog/catalog.interface';
|
||||||
|
import {
|
||||||
|
AVAILABLE_CATALOG_AVAILABILITY,
|
||||||
|
primaryAvailabilityMessage,
|
||||||
|
} from '../../../core/services/catalog/catalog-availability';
|
||||||
|
|
||||||
export interface Variant extends VariantSelectorVariant {
|
export interface Variant extends VariantSelectorVariant {
|
||||||
label?: string;
|
label?: string;
|
||||||
descripcion?: string | null;
|
descripcion?: string | null;
|
||||||
precio?: string | number;
|
precio?: string | number;
|
||||||
maximum_addable_quantity?: number | null;
|
availability?: CatalogAvailability;
|
||||||
unavailable_message?: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -36,8 +40,7 @@ export class ProductRowCardComponent {
|
|||||||
readonly title = input<string>('');
|
readonly title = input<string>('');
|
||||||
readonly description = input<string>('');
|
readonly description = input<string>('');
|
||||||
readonly price = input<number>(0);
|
readonly price = input<number>(0);
|
||||||
readonly maximumAddableQuantity = input<number | null>(null);
|
readonly availability = input<CatalogAvailability>(AVAILABLE_CATALOG_AVAILABILITY);
|
||||||
readonly unavailableMessage = input<string | null>(null);
|
|
||||||
readonly variants = input<Variant[]>([]);
|
readonly variants = input<Variant[]>([]);
|
||||||
readonly saving = input(false);
|
readonly saving = input(false);
|
||||||
|
|
||||||
@@ -60,17 +63,22 @@ export class ProductRowCardComponent {
|
|||||||
|
|
||||||
return Number.isFinite(variantPrice) ? variantPrice : this.price();
|
return Number.isFinite(variantPrice) ? variantPrice : this.price();
|
||||||
});
|
});
|
||||||
protected readonly effectiveMaximum = computed(
|
protected readonly effectiveAvailability = computed(
|
||||||
() => this.selectedVariantData()?.maximum_addable_quantity ?? this.maximumAddableQuantity(),
|
() =>
|
||||||
|
this.selectedVariantData()?.availability ??
|
||||||
|
this.availability() ??
|
||||||
|
AVAILABLE_CATALOG_AVAILABILITY,
|
||||||
|
);
|
||||||
|
protected readonly hasVariants = computed(() => this.variants().length > 0);
|
||||||
|
protected readonly hasPurchasableSelection = computed(
|
||||||
|
() => !this.hasVariants() || this.selectedVariantData() !== undefined,
|
||||||
|
);
|
||||||
|
protected readonly effectiveMaximum = computed(
|
||||||
|
() => this.effectiveAvailability().maximum_quantity,
|
||||||
|
);
|
||||||
|
protected readonly effectiveUnavailableMessage = computed(() =>
|
||||||
|
primaryAvailabilityMessage(this.effectiveAvailability()),
|
||||||
);
|
);
|
||||||
protected readonly unavailable = computed(() => this.effectiveMaximum() === 0);
|
|
||||||
protected readonly effectiveUnavailableMessage = computed(() => {
|
|
||||||
const selectedVariant = this.selectedVariantData();
|
|
||||||
|
|
||||||
return selectedVariant
|
|
||||||
? (selectedVariant.unavailable_message ?? null)
|
|
||||||
: this.unavailableMessage();
|
|
||||||
});
|
|
||||||
|
|
||||||
readonly formattedPrice = computed(() => this.formatCurrency(this.effectivePrice()));
|
readonly formattedPrice = computed(() => this.formatCurrency(this.effectivePrice()));
|
||||||
|
|
||||||
@@ -85,7 +93,11 @@ export class ProductRowCardComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected onAddToCart(): void {
|
protected onAddToCart(): void {
|
||||||
if (this.saving() || this.unavailable()) {
|
if (
|
||||||
|
this.saving() ||
|
||||||
|
!this.hasPurchasableSelection() ||
|
||||||
|
!this.effectiveAvailability().capabilities.add_to_cart
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +108,8 @@ export class ProductRowCardComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected onBuy(): void {
|
protected onBuy(): void {
|
||||||
if (this.unavailable()) return;
|
if (!this.hasPurchasableSelection() || !this.effectiveAvailability().capabilities.buy_now)
|
||||||
|
return;
|
||||||
|
|
||||||
this.buy.emit({
|
this.buy.emit({
|
||||||
quantity: this.quantity(),
|
quantity: this.quantity(),
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ 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 { 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';
|
||||||
@@ -353,7 +354,10 @@ export class ProductTicketSelectorComponent {
|
|||||||
if (reservedVariant !== null && !variants.some(({ id }) => id === reservedVariant.id)) {
|
if (reservedVariant !== null && !variants.some(({ id }) => id === reservedVariant.id)) {
|
||||||
variants.push(reservedVariant);
|
variants.push(reservedVariant);
|
||||||
}
|
}
|
||||||
return variants.filter(({ id }) => !reservedByOtherRows.has(id));
|
return variants.filter(
|
||||||
|
({ id, availability }) =>
|
||||||
|
availability?.capabilities.select_variant !== false && !reservedByOtherRows.has(id),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private reservedVariantData(row: TicketSelectionRow): CatalogFeaturedItemVariant | null {
|
private reservedVariantData(row: TicketSelectionRow): CatalogFeaturedItemVariant | null {
|
||||||
@@ -363,7 +367,7 @@ export class ProductTicketSelectorComponent {
|
|||||||
return {
|
return {
|
||||||
id: item.variant.id,
|
id: item.variant.id,
|
||||||
precio: item.variant.precio,
|
precio: item.variant.precio,
|
||||||
maximum_addable_quantity: item.variant.stock_tecnico,
|
availability: createCatalogAvailability(item.variant.stock_tecnico),
|
||||||
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]="unavailable()"
|
[disabled]="!effectiveAvailability().capabilities.change_quantity"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
@@ -29,12 +29,16 @@
|
|||||||
<app-quantity-selector
|
<app-quantity-selector
|
||||||
[(quantity)]="quantity"
|
[(quantity)]="quantity"
|
||||||
[max]="effectiveMaximum()"
|
[max]="effectiveMaximum()"
|
||||||
[disabled]="unavailable()"
|
[disabled]="!effectiveAvailability().capabilities.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 [variants]="variants()" [(selectedVariant)]="selectedVariant" />
|
<app-variant-selector
|
||||||
|
[variants]="variants()"
|
||||||
|
[disabled]="!availability().capabilities.select_variant"
|
||||||
|
[(selectedVariant)]="selectedVariant"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -42,14 +46,21 @@
|
|||||||
<div class="product-vertical-with-cart-card__actions">
|
<div class="product-vertical-with-cart-card__actions">
|
||||||
<app-button
|
<app-button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
[disabled]="unavailable() || (hasVariants() && selectedVariant() === null)"
|
[disabled]="
|
||||||
|
!effectiveAvailability().capabilities.buy_now ||
|
||||||
|
(hasVariants() && selectedVariant() === null)
|
||||||
|
"
|
||||||
(click)="onBuy()"
|
(click)="onBuy()"
|
||||||
>
|
>
|
||||||
Comprar
|
Comprar
|
||||||
</app-button>
|
</app-button>
|
||||||
<app-button
|
<app-button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
[disabled]="saving() || unavailable() || (hasVariants() && selectedVariant() === null)"
|
[disabled]="
|
||||||
|
saving() ||
|
||||||
|
!effectiveAvailability().capabilities.add_to_cart ||
|
||||||
|
(hasVariants() && selectedVariant() === null)
|
||||||
|
"
|
||||||
(click)="onAddToCart()"
|
(click)="onAddToCart()"
|
||||||
>
|
>
|
||||||
{{ saving() ? 'Guardando' : 'Agregar al carrito' }}
|
{{ saving() ? 'Guardando' : 'Agregar al carrito' }}
|
||||||
|
|||||||
@@ -15,12 +15,16 @@ import {
|
|||||||
VariantSelectorComponent,
|
VariantSelectorComponent,
|
||||||
VariantSelectorVariant,
|
VariantSelectorVariant,
|
||||||
} from '../variant-selector/variant-selector.component';
|
} from '../variant-selector/variant-selector.component';
|
||||||
|
import { CatalogAvailability } from '../../../core/services/catalog/catalog.interface';
|
||||||
|
import {
|
||||||
|
AVAILABLE_CATALOG_AVAILABILITY,
|
||||||
|
primaryAvailabilityMessage,
|
||||||
|
} from '../../../core/services/catalog/catalog-availability';
|
||||||
|
|
||||||
export interface VerticalCartVariant extends VariantSelectorVariant {
|
export interface VerticalCartVariant extends VariantSelectorVariant {
|
||||||
descripcion?: string | null;
|
descripcion?: string | null;
|
||||||
precio?: string | number;
|
precio?: string | number;
|
||||||
maximum_addable_quantity?: number | null;
|
availability?: CatalogAvailability;
|
||||||
unavailable_message?: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -34,8 +38,7 @@ export class ProductVerticalWithCartCardComponent {
|
|||||||
readonly title = input<string>('');
|
readonly title = input<string>('');
|
||||||
readonly description = input<string>('');
|
readonly description = input<string>('');
|
||||||
readonly price = input<number>(0);
|
readonly price = input<number>(0);
|
||||||
readonly maximumAddableQuantity = input<number | null>(null);
|
readonly availability = input<CatalogAvailability>(AVAILABLE_CATALOG_AVAILABILITY);
|
||||||
readonly unavailableMessage = input<string | null>(null);
|
|
||||||
readonly variants = input<VerticalCartVariant[]>([]);
|
readonly variants = input<VerticalCartVariant[]>([]);
|
||||||
readonly saving = input(false);
|
readonly saving = input(false);
|
||||||
|
|
||||||
@@ -58,17 +61,18 @@ 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 effectiveMaximum = computed(
|
protected readonly effectiveAvailability = computed(
|
||||||
() => this.selectedVariantData()?.maximum_addable_quantity ?? this.maximumAddableQuantity(),
|
() =>
|
||||||
|
this.selectedVariantData()?.availability ??
|
||||||
|
this.availability() ??
|
||||||
|
AVAILABLE_CATALOG_AVAILABILITY,
|
||||||
|
);
|
||||||
|
protected readonly effectiveMaximum = computed(
|
||||||
|
() => this.effectiveAvailability().maximum_quantity,
|
||||||
|
);
|
||||||
|
protected readonly effectiveUnavailableMessage = computed(() =>
|
||||||
|
primaryAvailabilityMessage(this.effectiveAvailability()),
|
||||||
);
|
);
|
||||||
protected readonly unavailable = computed(() => this.effectiveMaximum() === 0);
|
|
||||||
protected readonly effectiveUnavailableMessage = computed(() => {
|
|
||||||
const selectedVariant = this.selectedVariantData();
|
|
||||||
|
|
||||||
return selectedVariant
|
|
||||||
? (selectedVariant.unavailable_message ?? null)
|
|
||||||
: this.unavailableMessage();
|
|
||||||
});
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
effect(() => {
|
effect(() => {
|
||||||
@@ -81,7 +85,7 @@ export class ProductVerticalWithCartCardComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected onAddToCart(): void {
|
protected onAddToCart(): void {
|
||||||
if (this.saving() || this.unavailable()) {
|
if (this.saving() || !this.effectiveAvailability().capabilities.add_to_cart) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +93,7 @@ export class ProductVerticalWithCartCardComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected onBuy(): void {
|
protected onBuy(): void {
|
||||||
if (this.unavailable()) return;
|
if (!this.effectiveAvailability().capabilities.buy_now) return;
|
||||||
|
|
||||||
this.buy.emit({ quantity: this.quantity(), variant: this.selectedVariant() });
|
this.buy.emit({ quantity: this.quantity(), variant: this.selectedVariant() });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ export type VariantAttributeValue = VariantAttributeScalar | VariantAttributeSca
|
|||||||
export interface VariantSelectorVariant {
|
export interface VariantSelectorVariant {
|
||||||
id: unknown;
|
id: unknown;
|
||||||
values: Record<string, VariantAttributeValue>;
|
values: Record<string, VariantAttributeValue>;
|
||||||
|
availability?: {
|
||||||
|
capabilities: {
|
||||||
|
select_variant: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VariantSelectorSelectionChange {
|
export interface VariantSelectorSelectionChange {
|
||||||
@@ -64,10 +69,12 @@ export class VariantSelectorComponent {
|
|||||||
right: VariantAttributeValue | null,
|
right: VariantAttributeValue | null,
|
||||||
): boolean => left !== null && right !== null && this.sameValue(left, right);
|
): boolean => left !== null && right !== null && this.sameValue(left, right);
|
||||||
protected readonly attributeKeys = computed(() =>
|
protected readonly attributeKeys = computed(() =>
|
||||||
Array.from(new Set(this.variants().flatMap((variant) => Object.keys(variant.values)))),
|
Array.from(
|
||||||
|
new Set(this.getSelectableVariants().flatMap((variant) => Object.keys(variant.values))),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
protected readonly selectors = computed<VariantSelectorGroup[]>(() => {
|
protected readonly selectors = computed<VariantSelectorGroup[]>(() => {
|
||||||
const variants = this.variants();
|
const variants = this.getSelectableVariants();
|
||||||
const keys = this.attributeKeys();
|
const keys = this.attributeKeys();
|
||||||
const selectedValues = this.selectedValues();
|
const selectedValues = this.selectedValues();
|
||||||
|
|
||||||
@@ -110,7 +117,7 @@ export class VariantSelectorComponent {
|
|||||||
});
|
});
|
||||||
|
|
||||||
effect(() => {
|
effect(() => {
|
||||||
const variants = this.variants();
|
const variants = this.getSelectableVariants();
|
||||||
const selectedVariant = this.selectedVariant();
|
const selectedVariant = this.selectedVariant();
|
||||||
const autoSelectFirst = this.autoSelectFirst();
|
const autoSelectFirst = this.autoSelectFirst();
|
||||||
|
|
||||||
@@ -139,7 +146,7 @@ export class VariantSelectorComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected onValueChange(key: string, value: VariantAttributeValue | null): void {
|
protected onValueChange(key: string, value: VariantAttributeValue | null): void {
|
||||||
const variants = this.variants();
|
const variants = this.getSelectableVariants();
|
||||||
const keys = this.attributeKeys();
|
const keys = this.attributeKeys();
|
||||||
const changedIndex = keys.indexOf(key);
|
const changedIndex = keys.indexOf(key);
|
||||||
const values = { ...this.selectedValues() };
|
const values = { ...this.selectedValues() };
|
||||||
@@ -198,6 +205,12 @@ export class VariantSelectorComponent {
|
|||||||
return Array.from(options.values());
|
return Array.from(options.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getSelectableVariants(): VariantSelectorVariant[] {
|
||||||
|
return this.variants().filter(
|
||||||
|
(variant) => variant.availability?.capabilities.select_variant !== false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private reconcileManualSelection(
|
private reconcileManualSelection(
|
||||||
selectedValues: Record<string, VariantAttributeValue>,
|
selectedValues: Record<string, VariantAttributeValue>,
|
||||||
variants: VariantSelectorVariant[],
|
variants: VariantSelectorVariant[],
|
||||||
|
|||||||
Reference in New Issue
Block a user