feat(purchase-list): implement pagination for in-review and paid purchases

This commit is contained in:
2026-08-24 09:16:52 -03:00
parent d8d489e846
commit 870de3ca6e
4 changed files with 158 additions and 45 deletions

View File

@@ -2,6 +2,8 @@ import { Injectable } from '@angular/core';
import { firstValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { ApiPaginatedResponse } from './api-paginated-response.interface';
import { ApiPaginationQueryParams } from './api-pagination-query-params.interface';
import { BaseApiService } from './base-api.service'; import { BaseApiService } from './base-api.service';
export interface UpdatePurchaseCustomerPayload { export interface UpdatePurchaseCustomerPayload {
@@ -226,12 +228,20 @@ export class CheckoutService extends BaseApiService {
async getPurchases( async getPurchases(
tenantCode: string, tenantCode: string,
status?: string, status?: string,
): Promise<{ data: PurchaseSummaryResponse[] }> { pagination: ApiPaginationQueryParams = {},
let url = `${environment.url}tenants/${tenantCode}/compras`; ): Promise<ApiPaginatedResponse<PurchaseSummaryResponse[]>> {
if (status) { const params: Record<string, string | number> = {};
url += `?status=${status}`;
} if (status) params['status'] = status;
const response = await firstValueFrom(this.http.get<{ data: PurchaseSummaryResponse[] }>(url)); if (pagination.page) params['page'] = pagination.page;
if (pagination.per_page) params['per_page'] = pagination.per_page;
const response = await firstValueFrom(
this.http.get<ApiPaginatedResponse<PurchaseSummaryResponse[]>>(
`${environment.url}tenants/${tenantCode}/compras`,
{ params },
),
);
if (!response) { if (!response) {
throw new Error('Error al obtener las compras.'); throw new Error('Error al obtener las compras.');
} }

View File

@@ -1,29 +1,71 @@
<div class="purchase-list-container"> <div class="purchase-list-container">
<ng-container *ngIf="isLoading(); else contentTpl"> <section class="purchase-section" aria-labelledby="in-review-purchases-title">
<!-- Skeleton --> <h2 id="in-review-purchases-title" class="purchase-section__title">En revisión</h2>
<div class="skeleton-item" *ngFor="let i of [1, 2, 3, 4]">
<div class="skeleton-info">
<div class="skeleton-id"></div>
<div class="skeleton-date"></div>
</div>
<div class="skeleton-action"></div>
</div>
</ng-container>
<ng-template #contentTpl> @if (isLoadingInReview()) {
<ng-container *ngIf="purchases().length > 0; else emptyTpl"> <ng-container *ngTemplateOutlet="skeletonTpl"></ng-container>
<ng-container *ngFor="let purchase of purchases(); let last = last"> } @else {
<app-purchase-list-item @if (inReviewPurchases().length > 0) {
[purchase]="purchase"> @for (purchase of inReviewPurchases(); track purchase.id; let last = $last) {
</app-purchase-list-item> <app-purchase-list-item [purchase]="purchase"></app-purchase-list-item>
<div class="purchase-divider" *ngIf="!last"></div> @if (!last) {
</ng-container> <div class="purchase-divider"></div>
</ng-container> }
</ng-template> }
} @else {
<div class="empty-state">No hay compras en revisión</div>
}
<ng-template #emptyTpl> @if (inReviewPagination(); as pagination) {
<div class="empty-state"> <app-paginator
Aún no hay compras realizadas class="purchase-section__paginator"
[page]="pagination.current_page"
[totalPages]="pagination.last_page"
[disabled]="isLoadingInReview()"
(pageChange)="onInReviewPageChange($event)"
></app-paginator>
}
}
</section>
<section class="purchase-section" aria-labelledby="paid-purchases-title">
<h2 id="paid-purchases-title" class="purchase-section__title">Confirmadas</h2>
@if (isLoadingPaid()) {
<ng-container *ngTemplateOutlet="skeletonTpl"></ng-container>
} @else {
@if (paidPurchases().length > 0) {
@for (purchase of paidPurchases(); track purchase.id; let last = $last) {
<app-purchase-list-item [purchase]="purchase"></app-purchase-list-item>
@if (!last) {
<div class="purchase-divider"></div>
}
}
} @else {
<div class="empty-state">Aún no hay compras confirmadas</div>
}
@if (paidPagination(); as pagination) {
<app-paginator
class="purchase-section__paginator"
[page]="pagination.current_page"
[totalPages]="pagination.last_page"
[disabled]="isLoadingPaid()"
(pageChange)="onPaidPageChange($event)"
></app-paginator>
}
}
</section>
<ng-template #skeletonTpl>
@for (i of [1, 2, 3, 4]; track i) {
<div class="skeleton-item">
<div class="skeleton-info">
<div class="skeleton-id"></div>
<div class="skeleton-date"></div>
</div> </div>
</ng-template> <div class="skeleton-action"></div>
</div> </div>
}
</ng-template>
</div>

View File

@@ -1,9 +1,26 @@
.purchase-list-container { .purchase-list-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 2rem;
width: 100%; width: 100%;
} }
.purchase-section {
display: flex;
flex-direction: column;
}
.purchase-section__title {
margin: 0 0 0.75rem;
font-size: 1.125rem;
font-weight: 600;
}
.purchase-section__paginator {
align-self: center;
margin-top: 1.5rem;
}
.purchase-divider { .purchase-divider {
height: 1px; height: 1px;
background-color: #DDDDDD; background-color: #DDDDDD;

View File

@@ -4,6 +4,8 @@ import { PurchaseListItem } from '../purchase-list-item/purchase-list-item';
import { CheckoutService, PurchaseSummaryResponse } from '../../../../../../core/services/checkout.service'; import { CheckoutService, PurchaseSummaryResponse } from '../../../../../../core/services/checkout.service';
import { ToastService } from '../../../../../../core/services/toast.service'; import { ToastService } from '../../../../../../core/services/toast.service';
import { TenantService } from '../../../../../../core/services/tenant.service'; import { TenantService } from '../../../../../../core/services/tenant.service';
import { ApiPaginationMeta } from '../../../../../../core/services/api-paginated-response.interface';
import { PaginatorComponent } from '../../../../../../shared/components/paginator/paginator.component';
type PurchaseListViewModel = { type PurchaseListViewModel = {
id: number; id: number;
@@ -13,7 +15,7 @@ type PurchaseListViewModel = {
@Component({ @Component({
selector: 'app-purchase-list', selector: 'app-purchase-list',
standalone: true, standalone: true,
imports: [CommonModule, PurchaseListItem], imports: [CommonModule, PurchaseListItem, PaginatorComponent],
templateUrl: './purchase-list.html', templateUrl: './purchase-list.html',
styleUrl: './purchase-list.scss', styleUrl: './purchase-list.scss',
}) })
@@ -22,28 +24,70 @@ export class PurchaseList implements OnInit {
private readonly toastService = inject(ToastService); private readonly toastService = inject(ToastService);
private readonly tenantService = inject(TenantService); private readonly tenantService = inject(TenantService);
purchases = signal<PurchaseListViewModel[]>([]); protected readonly inReviewPurchases = signal<PurchaseListViewModel[]>([]);
isLoading = signal<boolean>(true); protected readonly paidPurchases = signal<PurchaseListViewModel[]>([]);
protected readonly inReviewPagination = signal<ApiPaginationMeta | null>(null);
protected readonly paidPagination = signal<ApiPaginationMeta | null>(null);
protected readonly isLoadingInReview = signal(true);
protected readonly isLoadingPaid = signal(true);
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
try { await Promise.all([this.loadInReviewPurchases(), this.loadPaidPurchases()]);
const tenantCode = this.tenantService.tenant()?.codigo || ''; }
protected async onInReviewPageChange(page: number): Promise<void> {
await this.loadInReviewPurchases(page);
}
protected async onPaidPageChange(page: number): Promise<void> {
await this.loadPaidPurchases(page);
}
private async loadInReviewPurchases(page = 1): Promise<void> {
this.isLoadingInReview.set(true);
try {
const response = await this.checkoutService const response = await this.checkoutService
.withCustomLoading() .withCustomLoading()
.getPurchases(tenantCode, 'paid'); .getPurchases(this.tenantCode(), 'in_review', { page });
const mappedPurchases = response.data.map((purchase: PurchaseSummaryResponse) => ({
id: purchase.id, this.inReviewPurchases.set(this.mapPurchases(response.data));
date: this.formatDate(purchase.created_at), this.inReviewPagination.set(response.meta);
}));
this.purchases.set(mappedPurchases);
} catch (error) { } catch (error) {
this.toastService.danger('Hubo un error al cargar las compras'); this.toastService.danger('Hubo un error al cargar las compras en revisión');
} finally { } finally {
this.isLoading.set(false); this.isLoadingInReview.set(false);
} }
} }
private async loadPaidPurchases(page = 1): Promise<void> {
this.isLoadingPaid.set(true);
try {
const response = await this.checkoutService
.withCustomLoading()
.getPurchases(this.tenantCode(), 'paid', { page });
this.paidPurchases.set(this.mapPurchases(response.data));
this.paidPagination.set(response.meta);
} catch (error) {
this.toastService.danger('Hubo un error al cargar las compras confirmadas');
} finally {
this.isLoadingPaid.set(false);
}
}
private tenantCode(): string {
return this.tenantService.tenant()?.codigo || '';
}
private mapPurchases(purchases: PurchaseSummaryResponse[]): PurchaseListViewModel[] {
return purchases.map((purchase) => ({
id: purchase.id,
date: this.formatDate(purchase.created_at),
}));
}
private formatDate(value: string | null): string { private formatDate(value: string | null): string {
if (!value) { if (!value) {
return '-'; return '-';