diff --git a/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.html b/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.html index f53dd7f..f34ff5a 100644 --- a/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.html +++ b/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.html @@ -574,24 +574,6 @@ -
-
- Reusable Component -

Ticket

- -

Ticket responsive que ocupa el 100% del ancho disponible.

- - -
-
-
Reusable Component diff --git a/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.ts b/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.ts index 8a255b6..de73780 100644 --- a/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.ts +++ b/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.ts @@ -14,7 +14,6 @@ import { TenantService } from '../../../../core/services/tenant.service'; import { ToastService } from '../../../../core/services/toast.service'; import { StepperComponent } from '../../../../shared/components/stepper/stepper.component'; import { StepComponent } from '../../../../shared/components/stepper/step.component'; -import { TicketComponent } from '../../../../shared/components/ticket/ticket.component'; @Component({ selector: 'app-reutilizables-test-page', @@ -31,7 +30,6 @@ import { TicketComponent } from '../../../../shared/components/ticket/ticket.com CartIconComponent, StepperComponent, StepComponent, - TicketComponent, ], templateUrl: './reutilizables-test-page.component.html', styleUrl: './reutilizables-test-page.component.scss', diff --git a/src/app/features/store/pages/account-page/components/account-sidebar/account-sidebar.html b/src/app/features/store/pages/account-page/components/account-sidebar/account-sidebar.html index 5750034..bcfe9dd 100644 --- a/src/app/features/store/pages/account-page/components/account-sidebar/account-sidebar.html +++ b/src/app/features/store/pages/account-page/components/account-sidebar/account-sidebar.html @@ -1,8 +1,11 @@ - + diff --git a/src/app/shared/components/ticket/ticket.component.html b/src/app/features/store/pages/account-page/pages/tickets-page/components/ticket/ticket.component.html similarity index 99% rename from src/app/shared/components/ticket/ticket.component.html rename to src/app/features/store/pages/account-page/pages/tickets-page/components/ticket/ticket.component.html index 520fdcc..d4ee560 100644 --- a/src/app/shared/components/ticket/ticket.component.html +++ b/src/app/features/store/pages/account-page/pages/tickets-page/components/ticket/ticket.component.html @@ -24,7 +24,6 @@ -
diff --git a/src/app/shared/components/ticket/ticket.component.scss b/src/app/features/store/pages/account-page/pages/tickets-page/components/ticket/ticket.component.scss similarity index 76% rename from src/app/shared/components/ticket/ticket.component.scss rename to src/app/features/store/pages/account-page/pages/tickets-page/components/ticket/ticket.component.scss index 6d24cd9..a338d48 100644 --- a/src/app/shared/components/ticket/ticket.component.scss +++ b/src/app/features/store/pages/account-page/pages/tickets-page/components/ticket/ticket.component.scss @@ -5,13 +5,12 @@ .ticket { display: grid; - grid-template-columns: auto minmax(0, 1fr) auto auto; + grid-template-columns: auto minmax(0, 1fr) 78px auto; align-items: center; width: 100%; - min-height: 56px; + min-height: 76px; gap: 16px; padding: 16px 0; - background: transparent; color: #666666; } @@ -22,8 +21,8 @@ } .ticket__selection input { - width: 16px; - height: 16px; + width: 21px; + height: 21px; margin: 0; accent-color: var(--tenant-primary, #009933); cursor: pointer; @@ -45,34 +44,28 @@ 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; + min-width: 115px; + min-height: 38px; padding-inline: 14px; } @@ -81,20 +74,22 @@ outline-offset: 2px; } -@media (max-width: 575.98px) { +@media (max-width: 767.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; } + + :host ::ng-deep .ticket__qr-button { + min-width: auto; + } } diff --git a/src/app/shared/components/ticket/ticket.component.spec.ts b/src/app/features/store/pages/account-page/pages/tickets-page/components/ticket/ticket.component.spec.ts similarity index 63% rename from src/app/shared/components/ticket/ticket.component.spec.ts rename to src/app/features/store/pages/account-page/pages/tickets-page/components/ticket/ticket.component.spec.ts index 04e5d7d..20e9709 100644 --- a/src/app/shared/components/ticket/ticket.component.spec.ts +++ b/src/app/features/store/pages/account-page/pages/tickets-page/components/ticket/ticket.component.spec.ts @@ -1,15 +1,16 @@ 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 () => { + it('renders its data and emits its actions', 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'); + fixture.componentRef.setInput('date', '09 de octubre'); const viewQr = vi.fn(); const download = vi.fn(); @@ -22,7 +23,6 @@ describe('TicketComponent', () => { 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('app-button button')?.click(); element.querySelector('[aria-label="Descargar ticket"]')?.click(); @@ -32,19 +32,4 @@ describe('TicketComponent', () => { 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); - }); }); diff --git a/src/app/shared/components/ticket/ticket.component.ts b/src/app/features/store/pages/account-page/pages/tickets-page/components/ticket/ticket.component.ts similarity index 74% rename from src/app/shared/components/ticket/ticket.component.ts rename to src/app/features/store/pages/account-page/pages/tickets-page/components/ticket/ticket.component.ts index 6f06061..f70f136 100644 --- a/src/app/shared/components/ticket/ticket.component.ts +++ b/src/app/features/store/pages/account-page/pages/tickets-page/components/ticket/ticket.component.ts @@ -1,6 +1,7 @@ import { ChangeDetectionStrategy, Component, input, model, output } from '@angular/core'; -import { ButtonComponent } from '../button/button.component'; -import { IconButtonComponent } from '../icon-button/icon-button.component'; + +import { ButtonComponent } from '../../../../../../../../shared/components/button/button.component'; +import { IconButtonComponent } from '../../../../../../../../shared/components/icon-button/icon-button.component'; @Component({ selector: 'app-ticket', diff --git a/src/app/features/store/pages/account-page/pages/tickets-page/ticket.service.ts b/src/app/features/store/pages/account-page/pages/tickets-page/ticket.service.ts new file mode 100644 index 0000000..66ce995 --- /dev/null +++ b/src/app/features/store/pages/account-page/pages/tickets-page/ticket.service.ts @@ -0,0 +1,33 @@ +import { HttpClient } from '@angular/common/http'; +import { inject, Injectable } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; + +import { TenantService } from '../../../../../../core/services/tenant.service'; + +export interface TicketResponse { + id: number; + tenant_code: string; + ticket: string; + name: string; + description: string | null; + source_catalog_item_id: number | null; + source_variant_id: number | null; + starts_at: string | null; + expires_at: string | null; + used_at: string | null; + is_valid: boolean; + is_expired: boolean; + is_used: boolean; +} + +@Injectable() +export class TicketService { + private readonly http = inject(HttpClient); + private readonly tenantService = inject(TenantService); + + getTickets(): Promise { + return firstValueFrom( + this.http.get<{ data: TicketResponse[] }>(`${this.tenantService.getTenantApiUrl()}/tickets`), + ).then((response) => response.data ?? []); + } +} diff --git a/src/app/features/store/pages/account-page/pages/tickets-page/tickets-page.html b/src/app/features/store/pages/account-page/pages/tickets-page/tickets-page.html new file mode 100644 index 0000000..eace3e8 --- /dev/null +++ b/src/app/features/store/pages/account-page/pages/tickets-page/tickets-page.html @@ -0,0 +1,46 @@ +
+

MIS TICKETS

+ +
+
+ + +
+ + + Descargar + + + + Compartir + +
+
+ + @if (isLoading()) { + @for (item of [1, 2, 3, 4]; track item) { + + } + } @else if (tickets().length) { + @for (ticket of tickets(); track ticket.id) { + + } + } @else { +

Aún no tenés tickets disponibles.

+ } +
+
diff --git a/src/app/features/store/pages/account-page/pages/tickets-page/tickets-page.scss b/src/app/features/store/pages/account-page/pages/tickets-page/tickets-page.scss new file mode 100644 index 0000000..cb56854 --- /dev/null +++ b/src/app/features/store/pages/account-page/pages/tickets-page/tickets-page.scss @@ -0,0 +1,81 @@ +.tickets-page { + width: 100%; + max-width: 720px; +} + +.tickets-page__title { + margin: 0 0 20px; + color: #a0a0a0; + font-size: 15px; + font-weight: 700; +} + +.tickets-list__header { + display: flex; + min-height: 58px; + align-items: center; + justify-content: space-between; + padding-bottom: 12px; + border-bottom: 1px solid #dddddd; +} + +.tickets-list__select-all { + display: flex; + align-items: center; + gap: 16px; + margin: 0; + color: #a0a0a0; + cursor: pointer; + font-size: 14px; + font-weight: 700; +} + +.tickets-list__select-all input { + width: 21px; + height: 21px; + margin: 0; + accent-color: var(--tenant-primary, #009933); +} + +.tickets-list__bulk-actions { + display: flex; + gap: 18px; +} + +.tickets-list__bulk-action { + display: flex; + flex-direction: column; + align-items: center; + color: #a0a0a0; + font-size: 11px; +} + +.tickets-list app-ticket { + border-bottom: 1px solid #dddddd; +} + +.tickets-list__empty { + padding: 32px 0; + color: #666666; + text-align: center; +} + +.tickets-list__skeleton { + height: 77px; + border-bottom: 1px solid #dddddd; + background: linear-gradient(90deg, transparent, #f3f3f3, transparent); + background-size: 200% 100%; + animation: loading 1.4s infinite; +} + +@keyframes loading { + to { + background-position: -200% 0; + } +} + +@media (max-width: 767.98px) { + .tickets-page__title { + margin-top: 24px; + } +} diff --git a/src/app/features/store/pages/account-page/pages/tickets-page/tickets-page.ts b/src/app/features/store/pages/account-page/pages/tickets-page/tickets-page.ts new file mode 100644 index 0000000..34852a6 --- /dev/null +++ b/src/app/features/store/pages/account-page/pages/tickets-page/tickets-page.ts @@ -0,0 +1,65 @@ +import { Component, computed, inject, OnInit, signal } from '@angular/core'; + +import { ToastService } from '../../../../../../core/services/toast.service'; +import { IconButtonComponent } from '../../../../../../shared/components/icon-button/icon-button.component'; +import { TicketComponent } from './components/ticket/ticket.component'; +import { TicketResponse, TicketService } from './ticket.service'; + +@Component({ + selector: 'app-tickets-page', + imports: [IconButtonComponent, TicketComponent], + providers: [TicketService], + templateUrl: './tickets-page.html', + styleUrl: './tickets-page.scss', +}) +export class TicketsPage implements OnInit { + private readonly ticketService = inject(TicketService); + private readonly toastService = inject(ToastService); + + protected readonly tickets = signal([]); + protected readonly selectedIds = signal>(new Set()); + protected readonly isLoading = signal(true); + protected readonly allSelected = computed( + () => this.tickets().length > 0 && this.selectedIds().size === this.tickets().length, + ); + + async ngOnInit(): Promise { + try { + this.tickets.set(await this.ticketService.getTickets()); + } catch { + this.toastService.danger('Hubo un error al cargar los tickets'); + } finally { + this.isLoading.set(false); + } + } + + protected toggleAll(selected: boolean): void { + this.selectedIds.set(selected ? new Set(this.tickets().map((ticket) => ticket.id)) : new Set()); + } + + protected toggleTicket(ticketId: number, selected: boolean): void { + this.selectedIds.update((current) => { + const next = new Set(current); + selected ? next.add(ticketId) : next.delete(ticketId); + return next; + }); + } + + protected isSelected(ticketId: number): boolean { + return this.selectedIds().has(ticketId); + } + + protected formatDate(ticket: TicketResponse): string | null { + const value = ticket.starts_at ?? ticket.expires_at; + if (!value) return null; + + const date = new Date(value); + if (Number.isNaN(date.getTime())) return null; + + return new Intl.DateTimeFormat('es-AR', { + day: '2-digit', + month: 'long', + timeZone: 'UTC', + }).format(date); + } +} diff --git a/src/app/features/store/store.routes.ts b/src/app/features/store/store.routes.ts index c6013a1..e570369 100644 --- a/src/app/features/store/store.routes.ts +++ b/src/app/features/store/store.routes.ts @@ -106,6 +106,14 @@ export const routes: Routes = [ (m) => m.PurchaseDetailPage, ), }, + { + path: 'tickets', + canActivate: [hasMenuGuard('tickets')], + loadComponent: () => + import('./pages/account-page/pages/tickets-page/tickets-page').then( + (m) => m.TicketsPage, + ), + }, { path: '', redirectTo: 'datos-personales',