diff --git a/src/app/shared/components/product-ticket-selector/product-ticket-selector.component.html b/src/app/shared/components/product-ticket-selector/product-ticket-selector.component.html
index a807a28..46ab67b 100644
--- a/src/app/shared/components/product-ticket-selector/product-ticket-selector.component.html
+++ b/src/app/shared/components/product-ticket-selector/product-ticket-selector.component.html
@@ -52,13 +52,12 @@
class="form-select variant-selector__select"
[attr.aria-label]="selector.label"
[disabled]="rowDisabled(row) || !selector.enabled"
- [compareWith]="compareValues"
- [ngModel]="selectedOption(row, selector.key)"
- (ngModelChange)="onSelectionChange(row.id, selector.key, $event)"
+ [value]="selectedOptionKey(row, selector.key)"
+ (change)="onSelectionKeyChange(row.id, selector, $any($event.target).value)"
>
-
+
@for (option of selector.options; track $index) {
-
+
}
}
diff --git a/src/app/shared/components/product-ticket-selector/product-ticket-selector.component.spec.ts b/src/app/shared/components/product-ticket-selector/product-ticket-selector.component.spec.ts
index f382f5b..1d31044 100644
--- a/src/app/shared/components/product-ticket-selector/product-ticket-selector.component.spec.ts
+++ b/src/app/shared/components/product-ticket-selector/product-ticket-selector.component.spec.ts
@@ -7,6 +7,7 @@ import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
import { CartService } from '../../../core/services/cart/cart.service';
import { CatalogService } from '../../../core/services/catalog/catalog.service';
+import { ModalService } from '../../../core/services/modal.service';
import { ProductTicketSelectorComponent } from './product-ticket-selector.component';
describe('ProductTicketSelectorComponent', () => {
@@ -21,6 +22,18 @@ describe('ProductTicketSelectorComponent', () => {
afterEach(() => TestBed.resetTestingModule());
it('checks partial selections and reserves the resolved variant in the cart', async () => {
+ const openConfirmDelete = vi.fn().mockReturnValue(of(true));
+ const removeItem = vi.fn().mockReturnValue(
+ of({
+ data: {
+ id: 10,
+ tenant_codigo: 'demo',
+ status: 'active',
+ subtotal: '0.00',
+ items: [],
+ },
+ }),
+ );
const resolvedVariant = {
id: 401,
precio: '10000.00',
@@ -101,6 +114,7 @@ describe('ProductTicketSelectorComponent', () => {
},
}),
),
+ removeItem,
};
catalogService.withoutLoading.mockReturnValue(catalogService);
cartService.withoutLoading.mockReturnValue(cartService);
@@ -109,6 +123,7 @@ describe('ProductTicketSelectorComponent', () => {
providers: [
{ provide: CatalogService, useValue: catalogService },
{ provide: CartService, useValue: cartService },
+ { provide: ModalService, useValue: { openConfirmDelete } },
],
}).compileComponents();
const fixture = TestBed.createComponent(ProductTicketSelectorComponent);
@@ -148,6 +163,23 @@ describe('ProductTicketSelectorComponent', () => {
expect(validation.querySelector('.fa-check')).not.toBeNull();
expect(fixture.nativeElement.querySelector('.ticket-selector__row-status')).toBeNull();
expect(fixture.nativeElement.querySelector('.ticket-selector__row-error')).toBeNull();
+
+ const deleteButton = fixture.nativeElement.querySelector(
+ 'app-icon-button button',
+ ) as HTMLButtonElement;
+ expect(deleteButton.classList.contains('icon-btn--bordered')).toBe(true);
+ deleteButton.click();
+
+ expect(openConfirmDelete).toHaveBeenCalledWith({
+ title: 'Eliminar entrada',
+ content:
+ 'Se eliminará esta entrada de “Entrada”. Si ya estaba reservada, se liberará del carrito.',
+ confirmLabel: 'Sí, eliminar',
+ cancelLabel: 'Cancelar',
+ size: 'md',
+ });
+ expect(removeItem).toHaveBeenCalledWith(25);
+ expect(fixture.componentInstance['rows']()).toHaveLength(0);
});
it('restores selections that are already reserved in the cart', async () => {
diff --git a/src/app/shared/components/product-ticket-selector/product-ticket-selector.component.ts b/src/app/shared/components/product-ticket-selector/product-ticket-selector.component.ts
index 415772a..f9db4cf 100644
--- a/src/app/shared/components/product-ticket-selector/product-ticket-selector.component.ts
+++ b/src/app/shared/components/product-ticket-selector/product-ticket-selector.component.ts
@@ -12,11 +12,12 @@ import {
signal,
untracked,
} from '@angular/core';
-import { FormsModule } from '@angular/forms';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Subscription } from 'rxjs';
import { CartService } from '../../../core/services/cart/cart.service';
import { CartItem, CartItemVariantValue } from '../../../core/services/cart/cart.interface';
+import { ModalService } from '../../../core/services/modal.service';
import {
CatalogVariantOptionsResponse,
CatalogVariantSelector,
@@ -49,7 +50,7 @@ interface TicketSelectionRow {
@Component({
selector: 'app-product-ticket-selector',
- imports: [ButtonComponent, IconButtonComponent, FormsModule],
+ imports: [ButtonComponent, IconButtonComponent],
templateUrl: './product-ticket-selector.component.html',
styleUrl: './product-ticket-selector.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -57,6 +58,7 @@ interface TicketSelectionRow {
export class ProductTicketSelectorComponent {
private readonly catalogService = inject(CatalogService);
private readonly cartService = inject(CartService);
+ private readonly modalService = inject(ModalService);
private readonly destroyRef = inject(DestroyRef);
private readonly optionRequests = new Map();
private readonly reservationRequests = new Map();
@@ -160,6 +162,23 @@ export class ProductTicketSelectorComponent {
const row = this.findRow(rowId);
if (!row || this.isBusy(row)) return;
+ this.modalService
+ .openConfirmDelete({
+ title: 'Eliminar entrada',
+ content: `Se eliminará esta entrada de “${this.title()}”. Si ya estaba reservada, se liberará del carrito.`,
+ confirmLabel: 'Sí, eliminar',
+ cancelLabel: 'Cancelar',
+ size: 'md',
+ })
+ .pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe((confirmed) => {
+ if (confirmed) this.confirmRemoveRow(row);
+ });
+ }
+
+ private confirmRemoveRow(row: TicketSelectionRow): void {
+ const rowId = row.id;
+
this.optionRequests.get(rowId)?.unsubscribe();
if (row.cartItemId === null) {
@@ -206,13 +225,6 @@ export class ProductTicketSelectorComponent {
this.loadOptions(rowId, selectedValues);
}
- protected readonly 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(', ');
@@ -222,6 +234,27 @@ export class ProductTicketSelectorComponent {
return row.selectedValues[key] ?? null;
}
+ protected optionKey(value: CatalogVariantValue): string {
+ return this.valueKey(value);
+ }
+
+ protected selectedOptionKey(row: TicketSelectionRow, key: string): string {
+ const selectedOption = this.selectedOption(row, key);
+
+ return selectedOption === null ? '' : this.valueKey(selectedOption);
+ }
+
+ protected onSelectionKeyChange(
+ rowId: number,
+ selector: CatalogVariantSelector,
+ selectedKey: string,
+ ): void {
+ const selectedOption =
+ selector.options.find((option) => this.valueKey(option) === selectedKey) ?? null;
+
+ this.onSelectionChange(rowId, selector.key, selectedOption);
+ }
+
protected rowDisabled(row: TicketSelectionRow): boolean {
return this.disabled() || this.isBusy(row);
}