feat(tickets): implement tickets page with ticket component and service

This commit is contained in:
2026-07-21 14:12:23 -03:00
parent 54826a7f8a
commit bc37a91c7f
12 changed files with 261 additions and 65 deletions

View File

@@ -1,30 +0,0 @@
<article class="ticket">
<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>
<p class="ticket__id">ID: {{ ticketId() }}</p>
</div>
@if (date(); as ticketDate) {
<time class="ticket__date">{{ ticketDate }}</time>
}
<div class="ticket__actions">
<app-button variant="primary" buttonClass="ticket__qr-button" (click)="viewQr.emit()">
<i class="fa-solid fa-qrcode" aria-hidden="true"></i>
<span>Ver QR</span>
</app-button>
<app-icon-button variant="download" ariaLabel="Descargar ticket" (clicked)="download.emit()" />
<app-icon-button variant="share" ariaLabel="Compartir ticket" (clicked)="share.emit()" />
</div>
</article>

View File

@@ -1,100 +0,0 @@
:host {
display: block;
width: 100%;
}
.ticket {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto auto;
align-items: center;
width: 100%;
min-height: 56px;
gap: 16px;
padding: 16px 0;
background: transparent;
color: #666666;
}
.ticket__selection {
display: flex;
margin: 0;
cursor: pointer;
}
.ticket__selection input {
width: 16px;
height: 16px;
margin: 0;
accent-color: var(--tenant-primary, #009933);
cursor: pointer;
}
.ticket__details {
min-width: 0;
}
.ticket__title,
.ticket__id {
overflow: hidden;
margin: 0;
text-overflow: ellipsis;
white-space: nowrap;
}
.ticket__title {
font-size: 13px;
font-weight: 700;
line-height: 1.25;
color: #666666;
}
.ticket__id {
font-size: 12px;
font-weight: 400;
line-height: 1.4;
color: #666666;
}
.ticket__date {
font-size: 10px;
font-weight: 400;
line-height: 1.4;
color: #666666;
white-space: nowrap;
}
.ticket__actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
:host ::ng-deep .ticket__qr-button {
min-width: auto;
min-height: 36px;
padding-inline: 14px;
}
.ticket__selection input:focus-visible {
outline: 2px solid var(--tenant-primary, #009933);
outline-offset: 2px;
}
@media (max-width: 575.98px) {
.ticket {
grid-template-columns: auto minmax(0, 1fr) auto;
gap: 10px;
padding: 20px 0;
}
.ticket__date {
grid-column: 2;
grid-row: 2;
}
.ticket__actions {
grid-column: 3;
grid-row: 1 / span 2;
}
}

View File

@@ -1,50 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { describe, expect, it, vi } from 'vitest';
import { TicketComponent } from './ticket.component';
describe('TicketComponent', () => {
it('renders the ticket data and emits every action', async () => {
await TestBed.configureTestingModule({ imports: [TicketComponent] }).compileComponents();
const fixture = TestBed.createComponent(TicketComponent);
fixture.componentRef.setInput('title', 'ENTRADA GENERAL');
fixture.componentRef.setInput('ticketId', '0000000000');
fixture.componentRef.setInput('date', '09 de Octubre');
const viewQr = vi.fn();
const download = vi.fn();
const share = vi.fn();
fixture.componentInstance.viewQr.subscribe(viewQr);
fixture.componentInstance.download.subscribe(download);
fixture.componentInstance.share.subscribe(share);
fixture.detectChanges();
const element = fixture.nativeElement as HTMLElement;
expect(element.textContent).toContain('ENTRADA GENERAL');
expect(element.textContent).toContain('ID: 0000000000');
expect(element.textContent).toContain('09 de Octubre');
element.querySelector<HTMLButtonElement>('app-button button')?.click();
element.querySelector<HTMLButtonElement>('[aria-label="Descargar ticket"]')?.click();
element.querySelector<HTMLButtonElement>('[aria-label="Compartir ticket"]')?.click();
expect(viewQr).toHaveBeenCalledOnce();
expect(download).toHaveBeenCalledOnce();
expect(share).toHaveBeenCalledOnce();
});
it('supports two-way selection state', async () => {
await TestBed.configureTestingModule({ imports: [TicketComponent] }).compileComponents();
const fixture = TestBed.createComponent(TicketComponent);
fixture.componentRef.setInput('ticketId', '0000000000');
fixture.componentRef.setInput('date', '09 de Octubre');
fixture.detectChanges();
const checkbox = fixture.nativeElement.querySelector('input') as HTMLInputElement;
checkbox.click();
fixture.detectChanges();
expect(fixture.componentInstance.selected()).toBe(true);
});
});

View File

@@ -1,21 +0,0 @@
import { ChangeDetectionStrategy, Component, input, model, output } from '@angular/core';
import { ButtonComponent } from '../button/button.component';
import { IconButtonComponent } from '../icon-button/icon-button.component';
@Component({
selector: 'app-ticket',
imports: [ButtonComponent, IconButtonComponent],
templateUrl: './ticket.component.html',
styleUrl: './ticket.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TicketComponent {
readonly title = input('ENTRADA GENERAL');
readonly ticketId = input.required<string>();
readonly date = input<string | null>(null);
readonly selected = model(false);
readonly viewQr = output<void>();
readonly download = output<void>();
readonly share = output<void>();
}