Reusable Component
Tarjetas de Producto (Column With Image)
- Las tarjetas se adaptan al tamano del contenedor padre. A continuacion se
- presentan dentro de una grilla de Bootstrap.
+ Las tarjetas se adaptan al tamano del contenedor padre. A continuacion se presentan dentro
+ de una grilla de Bootstrap.
@@ -636,9 +624,7 @@
Reusable Component
Tarjetas de Producto (Row Layout)
-
- Diseño horizontal que ocupa el 100% del ancho disponible.
-
+
Diseño horizontal que ocupa el 100% del ancho disponible.
@@ -678,10 +664,7 @@
-
+
@for (product of testProducts; track product.title) {
{
- this.lastModalResult = 'Simple modal cerrado';
- });
+ this.modalService
+ .openSimple({
+ title: 'Mensaje del sistema',
+ content: 'Este es un mensaje simple del sistema que no requiere confirmación.',
+ buttonLabel: 'Entendido',
+ })
+ .subscribe(() => {
+ this.lastModalResult = 'Simple modal cerrado';
+ });
}
private openConfirmModal(config: Parameters
[0]): void {
@@ -271,12 +257,9 @@ export class ReutilizablesTestPageComponent {
});
}
- private openConfirmDelete(
- config: Parameters[0]
- ): void {
+ private openConfirmDelete(config: Parameters[0]): void {
this.modalService.openConfirmDelete(config).subscribe((confirmed) => {
this.lastModalResult = `Resultado: ${confirmed}`;
});
}
}
-
diff --git a/src/app/shared/components/icon-button/icon-button.component.scss b/src/app/shared/components/icon-button/icon-button.component.scss
index 5ed4991..fe176c5 100644
--- a/src/app/shared/components/icon-button/icon-button.component.scss
+++ b/src/app/shared/components/icon-button/icon-button.component.scss
@@ -14,7 +14,9 @@
color: #666666;
cursor: pointer;
outline: none;
- transition: color 0.15s ease-in-out, transform 0.1s ease-in-out;
+ transition:
+ color 0.15s ease-in-out,
+ transform 0.1s ease-in-out;
border-radius: 4px;
i {
@@ -40,7 +42,7 @@
// Disabled state
&:disabled {
- color: #A0A0A0;
+ color: #a0a0a0;
cursor: not-allowed;
pointer-events: none;
}
@@ -74,3 +76,14 @@
font-size: 23px;
}
}
+
+.icon-btn--download,
+.icon-btn--share {
+ color: #a0a0a0;
+
+ i {
+ width: 15px;
+ height: 15px;
+ font-size: 15px;
+ }
+}
diff --git a/src/app/shared/components/icon-button/icon-button.component.ts b/src/app/shared/components/icon-button/icon-button.component.ts
index a899f80..516ed21 100644
--- a/src/app/shared/components/icon-button/icon-button.component.ts
+++ b/src/app/shared/components/icon-button/icon-button.component.ts
@@ -8,14 +8,16 @@ export type IconButtonVariant =
| 'angle-left'
| 'angle-up'
| 'angle-down'
- | 'copy';
+ | 'copy'
+ | 'download'
+ | 'share';
@Component({
selector: 'app-icon-button',
imports: [NgClass],
templateUrl: './icon-button.component.html',
styleUrl: './icon-button.component.scss',
- changeDetection: ChangeDetectionStrategy.OnPush
+ changeDetection: ChangeDetectionStrategy.OnPush,
})
export class IconButtonComponent {
readonly variant = input.required();
@@ -33,7 +35,9 @@ export class IconButtonComponent {
'angle-left': 'fa-solid fa-angle-left',
'angle-up': 'fa-solid fa-angle-up',
'angle-down': 'fa-solid fa-angle-down',
- copy: 'fa-solid fa-copy'
+ copy: 'fa-solid fa-copy',
+ download: 'fa-solid fa-download',
+ share: 'fa-solid fa-share-nodes',
};
return classes[this.variant()];
});
diff --git a/src/app/shared/components/ticket/ticket.component.html b/src/app/shared/components/ticket/ticket.component.html
new file mode 100644
index 0000000..520fdcc
--- /dev/null
+++ b/src/app/shared/components/ticket/ticket.component.html
@@ -0,0 +1,30 @@
+
+
+
+ Seleccionar ticket {{ title() }}
+
+
+
+
{{ title() }}
+
ID: {{ ticketId() }}
+
+
+ @if (date(); as ticketDate) {
+ {{ ticketDate }}
+ }
+
+
+
diff --git a/src/app/shared/components/ticket/ticket.component.scss b/src/app/shared/components/ticket/ticket.component.scss
new file mode 100644
index 0000000..6d24cd9
--- /dev/null
+++ b/src/app/shared/components/ticket/ticket.component.scss
@@ -0,0 +1,100 @@
+: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;
+ }
+}
diff --git a/src/app/shared/components/ticket/ticket.component.spec.ts b/src/app/shared/components/ticket/ticket.component.spec.ts
new file mode 100644
index 0000000..04e5d7d
--- /dev/null
+++ b/src/app/shared/components/ticket/ticket.component.spec.ts
@@ -0,0 +1,50 @@
+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('app-button button')?.click();
+ element.querySelector('[aria-label="Descargar ticket"]')?.click();
+ element.querySelector('[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);
+ });
+});
diff --git a/src/app/shared/components/ticket/ticket.component.ts b/src/app/shared/components/ticket/ticket.component.ts
new file mode 100644
index 0000000..6f06061
--- /dev/null
+++ b/src/app/shared/components/ticket/ticket.component.ts
@@ -0,0 +1,21 @@
+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();
+ readonly date = input(null);
+ readonly selected = model(false);
+
+ readonly viewQr = output();
+ readonly download = output();
+ readonly share = output();
+}