refactor(ticket-selector): consume compact remote options
This commit is contained in:
@@ -118,11 +118,23 @@ export interface CatalogFeaturedItemVariant {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface CatalogVariantOptionsResponse {
|
export interface CatalogVariantOptionsResponse {
|
||||||
variants: CatalogFeaturedItemVariant[];
|
selectors: CatalogVariantSelector[];
|
||||||
options: Record<string, CatalogVariantValue[]>;
|
|
||||||
selected_values: Record<string, string | string[]>;
|
selected_values: Record<string, string | string[]>;
|
||||||
resolved_variant_id: number | null;
|
resolved_variant: CatalogFeaturedItemVariant | null;
|
||||||
valid: boolean;
|
valid: boolean;
|
||||||
|
available_variant_count: number;
|
||||||
|
matching_variant_count: number;
|
||||||
|
price_range: {
|
||||||
|
minimum: string;
|
||||||
|
maximum: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CatalogVariantSelector {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
options: CatalogVariantValue[];
|
||||||
|
enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CatalogFeaturedItem {
|
export interface CatalogFeaturedItem {
|
||||||
|
|||||||
@@ -772,7 +772,6 @@
|
|||||||
[description]="testTicketSelectorProduct.description"
|
[description]="testTicketSelectorProduct.description"
|
||||||
[price]="testTicketSelectorProduct.price"
|
[price]="testTicketSelectorProduct.price"
|
||||||
[imageUrl]="testTicketSelectorProduct.imageUrl"
|
[imageUrl]="testTicketSelectorProduct.imageUrl"
|
||||||
[variants]="testTicketSelectorProduct.variants"
|
|
||||||
(buy)="onTicketBuy($event)"
|
(buy)="onTicketBuy($event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -34,8 +34,6 @@
|
|||||||
[description]="item.descripcion ?? ''"
|
[description]="item.descripcion ?? ''"
|
||||||
[price]="price(item)"
|
[price]="price(item)"
|
||||||
[imageUrl]="loadImages() ? (item.image ?? null) : null"
|
[imageUrl]="loadImages() ? (item.image ?? null) : null"
|
||||||
[variants]="item.variants ?? []"
|
|
||||||
[unavailableVariantIds]="unavailableVariantIds()"
|
|
||||||
[disabled]="loading()"
|
[disabled]="loading()"
|
||||||
(buy)="emitTicketBuy(item, $event)"
|
(buy)="emitTicketBuy(item, $event)"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -22,16 +22,23 @@
|
|||||||
<div class="ticket-selector__rows">
|
<div class="ticket-selector__rows">
|
||||||
@for (row of rows(); track row.id) {
|
@for (row of rows(); track row.id) {
|
||||||
<div class="ticket-selector__row">
|
<div class="ticket-selector__row">
|
||||||
<app-variant-selector
|
<div class="ticket-selector__fields variant-selector">
|
||||||
class="ticket-selector__fields"
|
@for (selector of row.selectors; track selector.key) {
|
||||||
[variants]="variantsForRow(row.id)"
|
<select
|
||||||
[disabled]="rowDisabled(row)"
|
class="form-select variant-selector__select"
|
||||||
[autoSelectFirst]="false"
|
[attr.aria-label]="selector.label"
|
||||||
[resetToken]="row.resetToken"
|
[disabled]="rowDisabled(row) || !selector.enabled"
|
||||||
[selectedVariant]="row.variantId"
|
[compareWith]="compareValues"
|
||||||
(selectedVariantChange)="updateCandidate(row.id, $event)"
|
[ngModel]="selectedOption(row, selector.key)"
|
||||||
(selectionValuesChange)="onSelectionChange(row.id, $event)"
|
(ngModelChange)="onSelectionChange(row.id, selector.key, $event)"
|
||||||
/>
|
>
|
||||||
|
<option [ngValue]="null" disabled>Seleccioná {{ selector.label }}</option>
|
||||||
|
@for (option of selector.options; track $index) {
|
||||||
|
<option [ngValue]="option">{{ optionLabel(option) }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
<app-icon-button
|
<app-icon-button
|
||||||
variant="trash"
|
variant="trash"
|
||||||
ariaLabel="Eliminar entrada"
|
ariaLabel="Eliminar entrada"
|
||||||
@@ -58,7 +65,7 @@
|
|||||||
+ Agregar entrada
|
+ Agregar entrada
|
||||||
</app-button>
|
</app-button>
|
||||||
|
|
||||||
@if (selectableVariants().length === 0) {
|
@if (availableVariantCount() === 0) {
|
||||||
<p class="ticket-selector__empty">No hay entradas disponibles.</p>
|
<p class="ticket-selector__empty">No hay entradas disponibles.</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,11 +80,23 @@
|
|||||||
|
|
||||||
&__fields {
|
&__fields {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(120px, 1fr));
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
::ng-deep .variant-selector {
|
.variant-selector__select {
|
||||||
display: grid;
|
width: 100%;
|
||||||
grid-template-columns: repeat(4, minmax(120px, 1fr));
|
min-width: 0;
|
||||||
gap: 0.5rem;
|
height: 38px;
|
||||||
|
color: #666;
|
||||||
|
border-color: var(--border-color);
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--tenant-primary);
|
||||||
|
box-shadow: 0 0 0 0.25rem rgba(0, 153, 51, 0.25);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,9 +136,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__fields {
|
&__fields {
|
||||||
::ng-deep .variant-selector {
|
grid-template-columns: 1fr;
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__map {
|
&__map {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
@@ -9,26 +10,18 @@ import {
|
|||||||
output,
|
output,
|
||||||
signal,
|
signal,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { HttpErrorResponse } from '@angular/common/http';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
import { CartService } from '../../../core/services/cart/cart.service';
|
import { CartService } from '../../../core/services/cart/cart.service';
|
||||||
|
import {
|
||||||
|
CatalogVariantOptionsResponse,
|
||||||
|
CatalogVariantSelector,
|
||||||
|
CatalogVariantValue,
|
||||||
|
} from '../../../core/services/catalog/catalog.interface';
|
||||||
import { CatalogService } from '../../../core/services/catalog/catalog.service';
|
import { CatalogService } from '../../../core/services/catalog/catalog.service';
|
||||||
import { ButtonComponent } from '../button/button.component';
|
import { ButtonComponent } from '../button/button.component';
|
||||||
import { IconButtonComponent } from '../icon-button/icon-button.component';
|
import { IconButtonComponent } from '../icon-button/icon-button.component';
|
||||||
import {
|
|
||||||
VariantAttributeScalar,
|
|
||||||
VariantAttributeValue,
|
|
||||||
VariantSelectorComponent,
|
|
||||||
VariantSelectorSelectionChange,
|
|
||||||
VariantSelectorVariant,
|
|
||||||
} from '../variant-selector/variant-selector.component';
|
|
||||||
|
|
||||||
export interface TicketSelectorVariant extends VariantSelectorVariant {
|
|
||||||
id: number;
|
|
||||||
precio?: string | number;
|
|
||||||
stock_tecnico?: number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
type TicketSelectionStatus =
|
type TicketSelectionStatus =
|
||||||
| 'selecting'
|
| 'selecting'
|
||||||
@@ -44,15 +37,14 @@ interface TicketSelectionRow {
|
|||||||
reservedVariantId: number | null;
|
reservedVariantId: number | null;
|
||||||
cartItemId: number | null;
|
cartItemId: number | null;
|
||||||
selectedValues: Record<string, string | string[]>;
|
selectedValues: Record<string, string | string[]>;
|
||||||
remoteVariants: TicketSelectorVariant[] | null;
|
selectors: CatalogVariantSelector[];
|
||||||
status: TicketSelectionStatus;
|
status: TicketSelectionStatus;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
resetToken: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-product-ticket-selector',
|
selector: 'app-product-ticket-selector',
|
||||||
imports: [ButtonComponent, IconButtonComponent, VariantSelectorComponent],
|
imports: [ButtonComponent, IconButtonComponent, FormsModule],
|
||||||
templateUrl: './product-ticket-selector.component.html',
|
templateUrl: './product-ticket-selector.component.html',
|
||||||
styleUrl: './product-ticket-selector.component.scss',
|
styleUrl: './product-ticket-selector.component.scss',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
@@ -69,22 +61,15 @@ export class ProductTicketSelectorComponent {
|
|||||||
readonly description = input<string>('');
|
readonly description = input<string>('');
|
||||||
readonly price = input<number>(0);
|
readonly price = input<number>(0);
|
||||||
readonly imageUrl = input<string | null>(null);
|
readonly imageUrl = input<string | null>(null);
|
||||||
readonly variants = input<TicketSelectorVariant[]>([]);
|
|
||||||
readonly unavailableVariantIds = input<ReadonlySet<number>>(new Set<number>());
|
|
||||||
readonly disabled = input(false);
|
readonly disabled = input(false);
|
||||||
|
|
||||||
readonly buy = output<number[]>();
|
readonly buy = output<number[]>();
|
||||||
|
|
||||||
protected readonly rows = signal<TicketSelectionRow[]>([this.createRow(1)]);
|
protected readonly rows = signal<TicketSelectionRow[]>([this.createRow(1)]);
|
||||||
|
protected readonly availableVariantCount = signal(0);
|
||||||
|
private readonly remotePriceRange = signal<{ minimum: number; maximum: number } | null>(null);
|
||||||
private nextRowId = 2;
|
private nextRowId = 2;
|
||||||
|
|
||||||
protected readonly selectableVariants = computed<TicketSelectorVariant[]>(() =>
|
|
||||||
this.variants().filter(
|
|
||||||
(variant) =>
|
|
||||||
!this.unavailableVariantIds().has(variant.id) &&
|
|
||||||
(variant.stock_tecnico == null || variant.stock_tecnico > 0),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
protected readonly hasSelection = computed(
|
protected readonly hasSelection = computed(
|
||||||
() =>
|
() =>
|
||||||
this.rows().length > 0 &&
|
this.rows().length > 0 &&
|
||||||
@@ -97,21 +82,15 @@ export class ProductTicketSelectorComponent {
|
|||||||
);
|
);
|
||||||
protected readonly canAddRow = computed(
|
protected readonly canAddRow = computed(
|
||||||
() =>
|
() =>
|
||||||
this.rows().length < this.selectableVariants().length &&
|
this.availableVariantCount() > 0 &&
|
||||||
|
this.rows().every((row) => row.status === 'reserved') &&
|
||||||
!this.rows().some((row) => this.isBusy(row)),
|
!this.rows().some((row) => this.isBusy(row)),
|
||||||
);
|
);
|
||||||
protected readonly priceRange = computed(() => {
|
protected readonly priceRange = computed(() => {
|
||||||
const prices = this.selectableVariants()
|
const range = this.remotePriceRange();
|
||||||
.map((variant) => Number(variant.precio ?? this.price()))
|
const minimum = range?.minimum ?? this.price();
|
||||||
.filter(Number.isFinite);
|
const maximum = range?.maximum ?? this.price();
|
||||||
|
|
||||||
if (prices.length === 0) {
|
|
||||||
const price = this.formatCurrency(this.price());
|
|
||||||
return { minimum: price, maximum: price, hasRange: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
const minimum = Math.min(...prices);
|
|
||||||
const maximum = Math.max(...prices);
|
|
||||||
return {
|
return {
|
||||||
minimum: this.formatCurrency(minimum),
|
minimum: this.formatCurrency(minimum),
|
||||||
maximum: this.formatCurrency(maximum),
|
maximum: this.formatCurrency(maximum),
|
||||||
@@ -121,20 +100,8 @@ export class ProductTicketSelectorComponent {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
effect(() => {
|
effect(() => {
|
||||||
const unavailable = this.unavailableVariantIds();
|
this.productId();
|
||||||
|
this.loadOptions(1, {});
|
||||||
this.rows.update((rows) =>
|
|
||||||
rows.map((row) =>
|
|
||||||
row.cartItemId === null && row.variantId !== null && unavailable.has(row.variantId)
|
|
||||||
? {
|
|
||||||
...row,
|
|
||||||
variantId: null,
|
|
||||||
status: 'error',
|
|
||||||
error: 'La entrada seleccionada ya no está disponible.',
|
|
||||||
}
|
|
||||||
: row,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.destroyRef.onDestroy(() => {
|
this.destroyRef.onDestroy(() => {
|
||||||
@@ -155,7 +122,10 @@ export class ProductTicketSelectorComponent {
|
|||||||
|
|
||||||
protected addRow(): void {
|
protected addRow(): void {
|
||||||
if (!this.canAddRow()) return;
|
if (!this.canAddRow()) return;
|
||||||
this.rows.update((rows) => [...rows, this.createRow(this.nextRowId++)]);
|
|
||||||
|
const row = this.createRow(this.nextRowId++);
|
||||||
|
this.rows.update((rows) => [...rows, row]);
|
||||||
|
this.loadOptions(row.id, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected removeRow(rowId: number): void {
|
protected removeRow(rowId: number): void {
|
||||||
@@ -171,7 +141,10 @@ export class ProductTicketSelectorComponent {
|
|||||||
|
|
||||||
this.patchRow(rowId, { status: 'removing', error: null });
|
this.patchRow(rowId, { status: 'removing', error: null });
|
||||||
const request = this.cartService.removeItem(row.cartItemId).subscribe({
|
const request = this.cartService.removeItem(row.cartItemId).subscribe({
|
||||||
next: () => this.deleteRow(rowId),
|
next: () => {
|
||||||
|
this.availableVariantCount.update((count) => count + 1);
|
||||||
|
this.deleteRow(rowId);
|
||||||
|
},
|
||||||
error: (error: HttpErrorResponse) => {
|
error: (error: HttpErrorResponse) => {
|
||||||
this.patchRow(rowId, {
|
this.patchRow(rowId, {
|
||||||
status: 'reserved',
|
status: 'reserved',
|
||||||
@@ -182,23 +155,61 @@ export class ProductTicketSelectorComponent {
|
|||||||
this.reservationRequests.set(rowId, request);
|
this.reservationRequests.set(rowId, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updateCandidate(rowId: number, selectedVariant: unknown): void {
|
protected onSelectionChange(
|
||||||
this.patchRow(rowId, {
|
rowId: number,
|
||||||
variantId: typeof selectedVariant === 'number' ? selectedVariant : null,
|
selectorKey: string,
|
||||||
});
|
value: CatalogVariantValue | null,
|
||||||
}
|
): void {
|
||||||
|
|
||||||
protected onSelectionChange(rowId: number, event: VariantSelectorSelectionChange): void {
|
|
||||||
const row = this.findRow(rowId);
|
const row = this.findRow(rowId);
|
||||||
if (!row || this.isBusy(row)) return;
|
if (!row || this.isBusy(row)) return;
|
||||||
|
|
||||||
const selectedValues = this.normalizeSelections(event.values);
|
const selectorIndex = row.selectors.findIndex((selector) => selector.key === selectorKey);
|
||||||
|
const retainedKeys = new Set(row.selectors.slice(0, selectorIndex + 1).map(({ key }) => key));
|
||||||
|
const selectedValues = Object.fromEntries(
|
||||||
|
Object.entries(row.selectedValues).filter(([key]) => retainedKeys.has(key)),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (value === null) delete selectedValues[selectorKey];
|
||||||
|
else selectedValues[selectorKey] = this.normalizeValue(value);
|
||||||
|
|
||||||
|
this.loadOptions(rowId, selectedValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected compareValues(
|
||||||
|
left: CatalogVariantValue | null,
|
||||||
|
right: CatalogVariantValue | null,
|
||||||
|
): boolean {
|
||||||
|
return left !== null && right !== null && this.valueKey(left) === this.valueKey(right);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected optionLabel(value: CatalogVariantValue): string {
|
||||||
|
const values = Array.isArray(value) ? value : [value];
|
||||||
|
return values.map((item) => (typeof item === 'string' ? item : item.label)).join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected selectedOption(row: TicketSelectionRow, key: string): string | string[] | null {
|
||||||
|
return row.selectedValues[key] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected rowDisabled(row: TicketSelectionRow): boolean {
|
||||||
|
return this.disabled() || this.isBusy(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected rowStatus(row: TicketSelectionRow): string | null {
|
||||||
|
if (row.status === 'checking') return 'Consultando disponibilidad…';
|
||||||
|
if (row.status === 'reserving') return 'Reservando entrada…';
|
||||||
|
if (row.status === 'removing') return 'Liberando entrada…';
|
||||||
|
if (row.status === 'reserved' && row.error === null) return 'Entrada reservada';
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadOptions(rowId: number, selectedValues: Record<string, string | string[]>): void {
|
||||||
|
const row = this.findRow(rowId);
|
||||||
|
if (!row) return;
|
||||||
|
|
||||||
this.optionRequests.get(rowId)?.unsubscribe();
|
this.optionRequests.get(rowId)?.unsubscribe();
|
||||||
this.patchRow(rowId, {
|
this.patchRow(rowId, { selectedValues, status: 'checking', error: null });
|
||||||
selectedValues,
|
|
||||||
status: 'checking',
|
|
||||||
error: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const request = this.catalogService
|
const request = this.catalogService
|
||||||
.getVariantOptions(this.productId(), {
|
.getVariantOptions(this.productId(), {
|
||||||
@@ -207,29 +218,32 @@ export class ProductTicketSelectorComponent {
|
|||||||
})
|
})
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (response) => {
|
next: (response) => {
|
||||||
const variants = response.variants as TicketSelectorVariant[];
|
this.applySummary(response, row);
|
||||||
|
|
||||||
if (!response.valid) {
|
if (!response.valid) {
|
||||||
this.patchRow(rowId, {
|
this.patchRow(rowId, {
|
||||||
variantId: null,
|
variantId: null,
|
||||||
selectedValues: {},
|
selectedValues: {},
|
||||||
remoteVariants: null,
|
selectors: response.selectors,
|
||||||
status: 'error',
|
status: response.available_variant_count === 0 ? 'selecting' : 'error',
|
||||||
error: 'La combinación seleccionada ya no está disponible.',
|
error:
|
||||||
resetToken: row.resetToken + 1,
|
response.available_variant_count === 0
|
||||||
|
? null
|
||||||
|
: 'La combinación seleccionada ya no está disponible.',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.patchRow(rowId, {
|
this.patchRow(rowId, {
|
||||||
variantId: response.resolved_variant_id,
|
variantId: response.resolved_variant?.id ?? null,
|
||||||
remoteVariants: variants,
|
selectedValues: response.selected_values,
|
||||||
|
selectors: response.selectors,
|
||||||
status: 'selecting',
|
status: 'selecting',
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.resolved_variant_id !== null) {
|
if (response.resolved_variant !== null) {
|
||||||
this.reserveRow(rowId, response.resolved_variant_id);
|
this.reserveRow(rowId, response.resolved_variant.id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (error: HttpErrorResponse) => {
|
error: (error: HttpErrorResponse) => {
|
||||||
@@ -242,34 +256,6 @@ export class ProductTicketSelectorComponent {
|
|||||||
this.optionRequests.set(rowId, request);
|
this.optionRequests.set(rowId, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected variantsForRow(rowId: number): VariantSelectorVariant[] {
|
|
||||||
const row = this.findRow(rowId);
|
|
||||||
const selectedByOtherRows = new Set(
|
|
||||||
this.rows()
|
|
||||||
.filter((candidate) => candidate.id !== rowId)
|
|
||||||
.flatMap((candidate) => {
|
|
||||||
const variantId = candidate.reservedVariantId ?? candidate.variantId;
|
|
||||||
return variantId === null ? [] : [variantId];
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const variants = row?.remoteVariants ?? this.selectableVariants();
|
|
||||||
|
|
||||||
return variants.filter((variant) => !selectedByOtherRows.has(variant.id as number));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected rowDisabled(row: TicketSelectionRow): boolean {
|
|
||||||
return this.disabled() || this.isBusy(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected rowStatus(row: TicketSelectionRow): string | null {
|
|
||||||
if (row.status === 'checking') return 'Consultando disponibilidad…';
|
|
||||||
if (row.status === 'reserving') return 'Reservando entrada…';
|
|
||||||
if (row.status === 'removing') return 'Liberando entrada…';
|
|
||||||
if (row.status === 'reserved' && row.error === null) return 'Entrada reservada';
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private reserveRow(rowId: number, variantId: number): void {
|
private reserveRow(rowId: number, variantId: number): void {
|
||||||
const row = this.findRow(rowId);
|
const row = this.findRow(rowId);
|
||||||
if (!row || (row.status === 'reserved' && row.reservedVariantId === variantId)) return;
|
if (!row || (row.status === 'reserved' && row.reservedVariantId === variantId)) return;
|
||||||
@@ -305,6 +291,9 @@ export class ProductTicketSelectorComponent {
|
|||||||
status: 'reserved',
|
status: 'reserved',
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
|
if (row.cartItemId === null) {
|
||||||
|
this.availableVariantCount.update((count) => Math.max(0, count - 1));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
error: (error: HttpErrorResponse) => {
|
error: (error: HttpErrorResponse) => {
|
||||||
this.patchRow(rowId, {
|
this.patchRow(rowId, {
|
||||||
@@ -317,6 +306,16 @@ export class ProductTicketSelectorComponent {
|
|||||||
this.reservationRequests.set(rowId, request);
|
this.reservationRequests.set(rowId, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private applySummary(response: CatalogVariantOptionsResponse, row: TicketSelectionRow): void {
|
||||||
|
this.availableVariantCount.set(
|
||||||
|
Math.max(0, response.available_variant_count - (row.cartItemId === null ? 0 : 1)),
|
||||||
|
);
|
||||||
|
this.remotePriceRange.set({
|
||||||
|
minimum: Number(response.price_range.minimum),
|
||||||
|
maximum: Number(response.price_range.maximum),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private createRow(id: number): TicketSelectionRow {
|
private createRow(id: number): TicketSelectionRow {
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
@@ -324,10 +323,9 @@ export class ProductTicketSelectorComponent {
|
|||||||
reservedVariantId: null,
|
reservedVariantId: null,
|
||||||
cartItemId: null,
|
cartItemId: null,
|
||||||
selectedValues: {},
|
selectedValues: {},
|
||||||
remoteVariants: null,
|
selectors: [],
|
||||||
status: 'selecting',
|
status: 'checking',
|
||||||
error: null,
|
error: null,
|
||||||
resetToken: 0,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,24 +347,20 @@ export class ProductTicketSelectorComponent {
|
|||||||
return ['checking', 'reserving', 'removing'].includes(row.status);
|
return ['checking', 'reserving', 'removing'].includes(row.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
private normalizeSelections(
|
private normalizeValue(value: CatalogVariantValue): string | string[] {
|
||||||
selections: Record<string, VariantAttributeValue>,
|
|
||||||
): Record<string, string | string[]> {
|
|
||||||
return Object.fromEntries(
|
|
||||||
Object.entries(selections).map(([key, value]) => [key, this.normalizeValue(value)]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private normalizeValue(value: VariantAttributeValue): string | string[] {
|
|
||||||
return Array.isArray(value)
|
return Array.isArray(value)
|
||||||
? value.map((item) => this.scalarValue(item))
|
? value.map((item) => this.scalarValue(item))
|
||||||
: this.scalarValue(value);
|
: this.scalarValue(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private scalarValue(value: VariantAttributeScalar): string {
|
private scalarValue(value: string | { value: string }): string {
|
||||||
return typeof value === 'string' ? value : value.value;
|
return typeof value === 'string' ? value : value.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private valueKey(value: CatalogVariantValue): string {
|
||||||
|
return JSON.stringify(this.normalizeValue(value));
|
||||||
|
}
|
||||||
|
|
||||||
private errorMessage(error: HttpErrorResponse, fallback: string): string {
|
private errorMessage(error: HttpErrorResponse, fallback: string): string {
|
||||||
return typeof error.error?.message === 'string' ? error.error.message : fallback;
|
return typeof error.error?.message === 'string' ? error.error.message : fallback;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user