feat(purchase-list): implement pagination for in-review and paid purchases
This commit is contained in:
@@ -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.');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]">
|
|
||||||
|
@if (isLoadingInReview()) {
|
||||||
|
<ng-container *ngTemplateOutlet="skeletonTpl"></ng-container>
|
||||||
|
} @else {
|
||||||
|
@if (inReviewPurchases().length > 0) {
|
||||||
|
@for (purchase of inReviewPurchases(); 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">No hay compras en revisión</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (inReviewPagination(); as pagination) {
|
||||||
|
<app-paginator
|
||||||
|
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-info">
|
||||||
<div class="skeleton-id"></div>
|
<div class="skeleton-id"></div>
|
||||||
<div class="skeleton-date"></div>
|
<div class="skeleton-date"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="skeleton-action"></div>
|
<div class="skeleton-action"></div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
}
|
||||||
|
|
||||||
<ng-template #contentTpl>
|
|
||||||
<ng-container *ngIf="purchases().length > 0; else emptyTpl">
|
|
||||||
<ng-container *ngFor="let purchase of purchases(); let last = last">
|
|
||||||
<app-purchase-list-item
|
|
||||||
[purchase]="purchase">
|
|
||||||
</app-purchase-list-item>
|
|
||||||
<div class="purchase-divider" *ngIf="!last"></div>
|
|
||||||
</ng-container>
|
|
||||||
</ng-container>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
<ng-template #emptyTpl>
|
|
||||||
<div class="empty-state">
|
|
||||||
Aún no hay compras realizadas
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,26 +24,68 @@ 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) => ({
|
|
||||||
|
this.inReviewPurchases.set(this.mapPurchases(response.data));
|
||||||
|
this.inReviewPagination.set(response.meta);
|
||||||
|
} catch (error) {
|
||||||
|
this.toastService.danger('Hubo un error al cargar las compras en revisión');
|
||||||
|
} finally {
|
||||||
|
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,
|
id: purchase.id,
|
||||||
date: this.formatDate(purchase.created_at),
|
date: this.formatDate(purchase.created_at),
|
||||||
}));
|
}));
|
||||||
this.purchases.set(mappedPurchases);
|
|
||||||
} catch (error) {
|
|
||||||
this.toastService.danger('Hubo un error al cargar las compras');
|
|
||||||
} finally {
|
|
||||||
this.isLoading.set(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private formatDate(value: string | null): string {
|
private formatDate(value: string | null): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user