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); }