feat(tickets): separate active and inactive tickets, update selection logic and styles
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
<article class="ticket" [class.ticket--disabled]="disabled()" [attr.aria-disabled]="disabled()">
|
||||
<label class="ticket__selection">
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="selected()"
|
||||
(change)="selected.set($any($event.target).checked)"
|
||||
/>
|
||||
<span class="visually-hidden">Seleccionar ticket {{ title() }}</span>
|
||||
</label>
|
||||
@if (!disabled()) {
|
||||
<label class="ticket__selection">
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="selected()"
|
||||
(change)="selected.set($any($event.target).checked)"
|
||||
/>
|
||||
<span class="visually-hidden">Seleccionar ticket {{ title() }}</span>
|
||||
</label>
|
||||
}
|
||||
|
||||
<div class="ticket__details">
|
||||
<h3 class="ticket__title">{{ title() }}</h3>
|
||||
|
||||
@@ -87,6 +87,7 @@
|
||||
}
|
||||
|
||||
.ticket--disabled {
|
||||
grid-template-columns: minmax(0, 1fr) 78px auto;
|
||||
color: #8a8a8a;
|
||||
}
|
||||
|
||||
@@ -116,4 +117,16 @@
|
||||
grid-column: 3;
|
||||
grid-row: 1 / span 2;
|
||||
}
|
||||
|
||||
.ticket--disabled {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.ticket--disabled .ticket__status {
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.ticket--disabled .ticket__actions {
|
||||
grid-column: 2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ describe('TicketComponent', () => {
|
||||
expect(qrButton?.disabled).toBe(true);
|
||||
expect(element.querySelector('[aria-label="Descargar ticket"]')).toBeNull();
|
||||
expect(element.querySelector('[aria-label="Compartir ticket"]')).toBeNull();
|
||||
expect(element.querySelector('input[type="checkbox"]')).toBeNull();
|
||||
|
||||
qrButton?.click();
|
||||
expect(viewQr).not.toHaveBeenCalled();
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="allSelected()"
|
||||
[disabled]="tickets().length === 0"
|
||||
[disabled]="activeTickets().length === 0"
|
||||
(change)="toggleAll($any($event.target).checked)"
|
||||
/>
|
||||
<span>TICKETS ACTIVOS</span>
|
||||
@@ -30,17 +30,33 @@
|
||||
<div class="tickets-list__skeleton" aria-hidden="true"></div>
|
||||
}
|
||||
} @else if (tickets().length) {
|
||||
@for (ticket of tickets(); track ticket.id) {
|
||||
@for (ticket of activeTickets(); track ticket.id) {
|
||||
<app-ticket
|
||||
[title]="ticket.name"
|
||||
[ticketId]="ticket.id"
|
||||
[date]="formatDate(ticket)"
|
||||
[disabled]="!ticket.is_valid"
|
||||
[expired]="ticket.is_expired"
|
||||
[selected]="isSelected(ticket.id)"
|
||||
(selectedChange)="toggleTicket(ticket.id, $event)"
|
||||
/>
|
||||
}
|
||||
|
||||
@if (inactiveTickets().length) {
|
||||
<section class="tickets-list__inactive" aria-labelledby="inactive-tickets-title">
|
||||
<header class="tickets-list__header tickets-list__header--inactive">
|
||||
<h2 id="inactive-tickets-title">TICKETS INACTIVOS</h2>
|
||||
</header>
|
||||
|
||||
@for (ticket of inactiveTickets(); track ticket.id) {
|
||||
<app-ticket
|
||||
[title]="ticket.name"
|
||||
[ticketId]="ticket.id"
|
||||
[date]="formatDate(ticket)"
|
||||
[disabled]="true"
|
||||
[expired]="ticket.is_expired"
|
||||
/>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
} @else {
|
||||
<p class="tickets-list__empty">Aún no tenés tickets disponibles.</p>
|
||||
}
|
||||
|
||||
@@ -19,6 +19,21 @@
|
||||
border-bottom: 1px solid #dddddd;
|
||||
}
|
||||
|
||||
.tickets-list__header--inactive {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.tickets-list__header--inactive h2 {
|
||||
margin: 0;
|
||||
color: #a0a0a0;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.tickets-list__inactive {
|
||||
margin-top: 44px;
|
||||
}
|
||||
|
||||
.tickets-list__select-all {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { ToastService } from '../../../../../../core/services/toast.service';
|
||||
import { TicketComponent } from './components/ticket/ticket.component';
|
||||
import { TicketResponse, TicketService } from './ticket.service';
|
||||
import { TicketsPage } from './tickets-page';
|
||||
|
||||
const ticket = (overrides: Partial<TicketResponse>): TicketResponse => ({
|
||||
id: 1,
|
||||
tenant_code: 'tenant',
|
||||
ticket: 'ticket-1',
|
||||
name: 'Entrada general',
|
||||
description: null,
|
||||
source_catalog_item_id: null,
|
||||
source_variant_id: null,
|
||||
starts_at: '2026-10-09T00:00:00.000Z',
|
||||
expires_at: null,
|
||||
used_at: null,
|
||||
is_valid: true,
|
||||
is_expired: false,
|
||||
is_used: false,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('TicketsPage', () => {
|
||||
it('separates expired and used tickets into the disabled inactive list', async () => {
|
||||
const tickets = [
|
||||
ticket({ id: 1 }),
|
||||
ticket({ id: 2, is_valid: false, is_expired: true }),
|
||||
ticket({ id: 3, is_valid: false, is_used: true, used_at: '2026-10-10T00:00:00.000Z' }),
|
||||
];
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TicketsPage],
|
||||
providers: [{ provide: ToastService, useValue: { danger: vi.fn() } }],
|
||||
})
|
||||
.overrideComponent(TicketsPage, {
|
||||
set: {
|
||||
providers: [
|
||||
{ provide: TicketService, useValue: { getTickets: () => Promise.resolve(tickets) } },
|
||||
],
|
||||
},
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
const fixture = TestBed.createComponent(TicketsPage);
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
const ticketComponents = fixture.debugElement.queryAll(By.directive(TicketComponent));
|
||||
const [activeTicket, expiredTicket, usedTicket] = ticketComponents.map(
|
||||
(element) => element.componentInstance as TicketComponent,
|
||||
);
|
||||
|
||||
expect(ticketComponents).toHaveLength(3);
|
||||
expect(activeTicket.disabled()).toBe(false);
|
||||
expect(expiredTicket.disabled()).toBe(true);
|
||||
expect(expiredTicket.expired()).toBe(true);
|
||||
expect(usedTicket.disabled()).toBe(true);
|
||||
expect(usedTicket.expired()).toBe(false);
|
||||
expect(fixture.nativeElement.textContent).toContain('TICKETS INACTIVOS');
|
||||
|
||||
const page = fixture.nativeElement as HTMLElement;
|
||||
const inactiveElements = Array.from(
|
||||
page.querySelectorAll<HTMLElement>('.tickets-list__inactive app-ticket'),
|
||||
);
|
||||
expect(inactiveElements).toHaveLength(2);
|
||||
inactiveElements.forEach((element) => {
|
||||
expect(element.querySelector('.ticket--disabled')).not.toBeNull();
|
||||
expect(element.querySelector<HTMLButtonElement>('app-button button')?.disabled).toBe(true);
|
||||
expect(element.querySelector('input[type="checkbox"]')).toBeNull();
|
||||
expect(element.querySelector('[aria-label="Descargar ticket"]')).toBeNull();
|
||||
expect(element.querySelector('[aria-label="Compartir ticket"]')).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -17,10 +17,17 @@ export class TicketsPage implements OnInit {
|
||||
private readonly toastService = inject(ToastService);
|
||||
|
||||
protected readonly tickets = signal<TicketResponse[]>([]);
|
||||
protected readonly activeTickets = computed(() =>
|
||||
this.tickets().filter((ticket) => !this.isInactive(ticket)),
|
||||
);
|
||||
protected readonly inactiveTickets = computed(() =>
|
||||
this.tickets().filter((ticket) => this.isInactive(ticket)),
|
||||
);
|
||||
protected readonly selectedIds = signal<Set<number>>(new Set());
|
||||
protected readonly isLoading = signal(true);
|
||||
protected readonly allSelected = computed(
|
||||
() => this.tickets().length > 0 && this.selectedIds().size === this.tickets().length,
|
||||
() =>
|
||||
this.activeTickets().length > 0 && this.selectedIds().size === this.activeTickets().length,
|
||||
);
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
@@ -34,7 +41,9 @@ export class TicketsPage implements OnInit {
|
||||
}
|
||||
|
||||
protected toggleAll(selected: boolean): void {
|
||||
this.selectedIds.set(selected ? new Set(this.tickets().map((ticket) => ticket.id)) : new Set());
|
||||
this.selectedIds.set(
|
||||
selected ? new Set(this.activeTickets().map((ticket) => ticket.id)) : new Set(),
|
||||
);
|
||||
}
|
||||
|
||||
protected toggleTicket(ticketId: number, selected: boolean): void {
|
||||
@@ -49,6 +58,10 @@ export class TicketsPage implements OnInit {
|
||||
return this.selectedIds().has(ticketId);
|
||||
}
|
||||
|
||||
protected isInactive(ticket: TicketResponse): boolean {
|
||||
return ticket.is_expired || ticket.is_used;
|
||||
}
|
||||
|
||||
protected formatDate(ticket: TicketResponse): string | null {
|
||||
const value = ticket.starts_at ?? ticket.expires_at;
|
||||
if (!value) return null;
|
||||
|
||||
Reference in New Issue
Block a user