Using Angular, I am consuming an API from ASP.NET Core Web API, I have this endpoint that performs Query Filter, ngx-Pagination, ngx-datepicker, and Sorting. It can perform
- Search Query,
- Search Query and Export to Excel
GET Method (example of the same endpoint):
https://localhost:7244/api/v1/transactions/all-transactions-by-created_date?StartDate=2022-01-01&EndDate=2023-01-01&exportToExcel=false
https://localhost:7244/api/v1/transactions/all-transactions-by-created_date?exportToExcel=false
https://localhost:7244/api/v1/transactions/all-transactions-by-created_date?SortBy=Ascending&IsSortAscending=true&exportToExcel=false
https://localhost:7244/api/v1/transactions/all-transactions-by-created_date?SearchQuery=428ce81ffa8742f2b6779ba504eb4d80&StartDate=2022-02-02&EndDate=2025-01-01&exportToExcel=false
JSON Response:
{
"data": {
"pageItems": [
{
"id": 1073025,
"aggregatorId": "fdfdfdfdassa",
"acctNo": "5432156",
"amount": 3000,
"acctType": null,
"source": "LEMMY",
"sourcePtid": 1.24110443187,
"transactionType": "C",
"description": "DSDSDSDSDSDS",
"createDt": "2024-11-12T16:46:16.877",
"effectiveDt": "2024-11-12T16:46:16.877",
"tfrAcctNo": null,
"currency": null,
"tranCode": null,
"originTracerNo": "DSDSDSDSDSDS",
"channelId": 0,
"transactionId": "SDSDSDSDSDS",
"payerAcctNo": null,
"payerName": "DSDSDS HGHGHG",
"originBankCode": "000001",
"notified": "N",
"notifiedDt": "2025-01-24T11:26:20.55",
"channelName": "NIP",
"charges": 0,
"instantSettlementCount": 0,
"instantSettledStatus": null,
"instantSettledMessage": null,
"instantSettlementPostingDate": "2025-01-24T11:26:20.55",
"isRequeried": false,
"instantSettlementProcessingCount": 0,
"instantSettlementProcessingNextRuntime": "0001-01-01T00:00:00",
"stagingDt": "2025-01-24T11:26:20.55",
"insertDtTm": "2024-11-12T16:46:16.95",
"errorText": null,
"errorDt": "0001-01-01T00:00:00",
"notifyCount": 0,
"fcubeReference": "M243170707406487",
"cbaReference": "MAS243170707409465",
"xapiBatchId": "117853997723599116619010321868",
"settlementAccountNumber": null
},
...
],
"currentPage": 1,
"pageSize": 10,
"numberOfPages": 1385,
"totalRecord": 13849,
"previousPage": 0
},
"successful": true,
"message": "All Transactions Retrieved Successfully",
"statusCode": 200,
"responseCode": null,
"responseDescription": null
}
I am consuming the Asp.NET Core Web API using Angular: with ngx-pagination, ngx-datepicker, page filter (10, 50, 100) ItemPerPage, Download to Excel, StartDate and EndDate must be separate.
- And should not allow future Date, and user cannot backdate beyond one year
- By default no data should be loaded on the Page until when the Search Button is Submitted
- The Search and ExportToExcel buttons will not be enabled until when user select StartDate and EndDate (they are both required).
You can see it in the screenshot.
MAIN CODE:
export interface IPageChangedEvent {
itemsPerPage: number;
page: number;
}
admin-transaction-created-date-list.model.ts:
export interface IAdminTransactionCreatedDateList {
aggregatorId: string;
acctNo: string;
amount: number;
acctType: string | null;
source: string;
sourcePtid: number;
transactionType: string;
description: string;
createDt: string;
effectiveDt: string;
tfrAcctNo: string | null;
currency: string | null;
tranCode: string | null;
originTracerNo: string;
channelId: number;
transactionId: string;
payerAcctNo: string | null;
payerName: string;
originBankCode: string;
notified: string;
notifiedDt: string;
channelName: string;
charges: number;
thirdPartyComm: number;
instantSettlementCount: number;
instantSettledStatus: string;
instantSettledMessage: string;
instantSettlementPostingDate: string;
isRequeried: boolean;
instantSettlementProcessingCount: number;
instantSettlementProcessingNextRuntime: string;
stagingDt: string;
insertDtTm: string;
errorText: string;
errorDt: string;
notifyCount: number;
fcubeReference: string;
cbaReference: string;
xapiBatchId: string;
settlementAccountNumber: string | null;
}
export interface PageResult<T> {
pageItems: T[];
currentPage: number;
pageSize: number;
numberOfPages: number;
totalRecord: number;
previousPage: number;
}
export interface IAdminTransactionCreatedDateResponse<T> {
data: PageResult<T>;
successful: boolean;
message: string;
statusCode: number;
responseCode: string | null;
responseDescription: string | null;
}
export interface IAdminTransactionCreatedDateFilter {
pageNumber: number;
pageSize: number;
searchQuery?: string;
startDate: Date | null;
endDate: Date | null;
sortBy?: string;
isSortAscending?: boolean;
}
admin-transaction-service.ts:
import { Injectable } from '@angular/core';
import { environment } from 'src/environments/environment';
import { Observable } from 'rxjs';
import {
HttpClient,
HttpHeaders,
HttpParams,
HttpResponse,
} from '@angular/common/http';
import {
IAdminTransactionCreatedDateList,
IAdminTransactionCreatedDateResponse,
IAdminTransactionCreatedDateFilter
} from '../../../features/admin/models/transaction/admin-transaction-created-date-list.model';
@Injectable({
providedIn: 'root'
})
export class AdminTransactionService {
baseUrl = environment.baseHost;
token = localStorage.getItem('token');
constructor(private http: HttpClient) {}
getAllTransactions(filter: IAdminTransactionCreatedDateFilter): Observable<IAdminTransactionCreatedDateResponse<IAdminTransactionCreatedDateList>> {
let params = new HttpParams()
.set('PageNumber', filter.pageNumber.toString())
.set('PageSize', filter.pageSize.toString())
.set('exportToExcel', 'false');
if (filter.searchQuery) {
params = params.set('SearchQuery', filter.searchQuery);
}
if (filter.sortBy) {
params = params.set('SortBy', filter.sortBy);
params = params.set('IsSortAscending', filter.isSortAscending?.toString() || 'false');
}
if (filter.startDate) {
params = params.set('StartDate', filter.startDate.toISOString());
}
if (filter.endDate) {
params = params.set('EndDate', filter.endDate.toISOString());
}
return this.http.get<IAdminTransactionCreatedDateResponse<IAdminTransactionCreatedDateList>>(`${this.baseUrl}/transactions/all-transactions-by-created_date`, { params });
}
exportToExcel(filter: IAdminTransactionCreatedDateFilter): Observable<Blob> {
let params = new HttpParams()
.set('PageNumber', filter.pageNumber.toString())
.set('PageSize', filter.pageSize.toString())
.set('exportToExcel', 'true');
if (filter.searchQuery) {
params = params.set('SearchQuery', filter.searchQuery);
}
if (filter.startDate) {
params = params.set('StartDate', filter.startDate.toISOString());
}
if (filter.endDate) {
params = params.set('EndDate', filter.endDate.toISOString());
}
return this.http.get(`${this.baseUrl}/transactions/all-transactions-by-created_date`,
{ params, responseType: 'blob' }
);
}
}
admin-created-date-transactions.ts:
import { Component, OnInit } from '@angular/core';
import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
import { FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms';
import { ToastrService } from 'ngx-toastr';
import { AdminTransactionService } from 'src/app/features/admin/services/admin-transaction.service';
import {
IAdminTransactionCreatedDateList,
IAdminTransactionCreatedDateFilter
} from 'src/app/features/admin/models/transaction/admin-transaction-created-date-list.model';
import { BsDatepickerConfig } from 'ngx-bootstrap/datepicker';
import { saveAs } from 'file-saver';
import * as moment from 'moment';
import { IPageChangedEvent } from 'src/app/shared/models/page-changed-event.model';
@Component({
selector: 'app-admin-created-date-transactions',
templateUrl: './admin-created-date-transactionsponent.html',
styleUrls: ['./admin-created-date-transactionsponent.scss']
})
export class AdminCreatedDateTransactionsComponent implements OnInit {
bsModalRef?: BsModalRef;
pageTitle = 'Transaction By Created Date';
pageLabel = 'Transaction By Created Date List';
advanceSearch = 'Advance Search';
search = 'Search';
searchForm!: FormGroup;
transactions: IAdminTransactionCreatedDateList[] = [];
currentPage = 1;
pageSize = 10;
totalRecords = 0;
hasSearchResults = false;
maxDate = new Date();
minDate = new Date();
loading = false;
pageSizeOptions = [10, 50, 100];
bsConfig!: Partial<BsDatepickerConfig>;
Math = Math;
constructor(
private fb: FormBuilder,
private transactionService: AdminTransactionService,
private toastr: ToastrService
) {
this.minDate.setFullYear(this.minDate.getFullYear() - 1);
this.bsConfig = {
dateInputFormat: 'DD-MMM-YYYY',
isAnimated: true,
returnFocusToInput: true,
showClearButton: true,
showWeekNumbers: false,
adaptivePosition: true,
containerClass: 'theme-dark-blue'
};
this.initializeForm();
}
ngOnInit(): void {
this.setupFormValidation();
}
private initializeForm(): void {
this.searchForm = this.fb.group({
startDate: [null, [Validators.required]],
endDate: [null, [Validators.required]],
searchQuery: [''],
pageSize: [this.pageSize],
sortDirection: ['Ascending']
}, { validator: this.dateRangeValidator });
}
private setupFormValidation(): void {
this.searchForm.get('startDate')?.valueChanges.subscribe(() => {
this.searchForm.get('endDate')?.updateValueAndValidity();
});
this.searchForm.get('endDate')?.valueChanges.subscribe(() => {
if (this.searchForm.get('startDate')?.value) {
this.validateDateRange();
}
});
}
private dateRangeValidator(group: FormGroup): {[key: string]: any} | null {
const start = group.get('startDate')?.value;
const end = group.get('endDate')?.value;
if (start && end) {
const startDate = new Date(start);
const endDate = new Date(end);
if (startDate > endDate) {
return { dateRange: true };
}
const oneYearAgo = new Date();
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
if (startDate < oneYearAgo) {
return { tooOld: true };
}
}
return null;
}
private validateDateRange(): void {
const startDate = this.searchForm.get('startDate')?.value;
const endDate = this.searchForm.get('endDate')?.value;
if (startDate && endDate) {
const start = new Date(startDate);
const end = new Date(endDate);
const oneYearAgo = new Date();
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
if (start > end) {
this.searchForm.get('endDate')?.setErrors({ dateRange: true });
} else if (start < oneYearAgo) {
this.searchForm.get('startDate')?.setErrors({ tooOld: true });
} else {
this.searchForm.get('endDate')?.setErrors(null);
this.searchForm.get('startDate')?.setErrors(null);
}
}
}
onSearch(): void {
if (this.searchForm.valid) {
this.currentPage = 1;
this.loadTransactions();
} else {
this.markFormGroupTouched(this.searchForm);
}
}
private markFormGroupTouched(formGroup: FormGroup) {
Object.values(formGroup.controls).forEach(control => {
control.markAsTouched();
if (control instanceof FormGroup) {
this.markFormGroupTouched(control);
}
});
}
loadTransactions(): void {
this.loading = true;
const filter: IAdminTransactionCreatedDateFilter = {
pageNumber: this.currentPage,
pageSize: Number(this.searchForm.get('pageSize')?.value),
startDate: this.searchForm.get('startDate')?.value,
endDate: this.searchForm.get('endDate')?.value,
searchQuery: this.searchForm.get('searchQuery')?.value,
sortBy: this.searchForm.get('sortDirection')?.value,
isSortAscending: this.searchForm.get('sortDirection')?.value === 'Ascending'
};
this.transactionService.getAllTransactions(filter).subscribe({
next: (response) => {
this.transactions = response.data.pageItems;
this.totalRecords = response.data.totalRecord;
this.hasSearchResults = true;
this.loading = false;
if (this.transactions.length === 0) {
this.toastr.info('No transactions found for the selected criteria');
}
},
error: (error) => {
this.loading = false;
this.toastr.error('Error loading transactions');
}
});
}
onPageChange(event: IPageChangedEvent): void {
this.currentPage = event.page;
this.loadTransactions();
}
onPageSizeChange(): void {
this.currentPage = 1;
this.pageSize = Number(this.searchForm.get('pageSize')?.value);
if (this.hasSearchResults) {
this.loadTransactions();
}
}
onSortChange(): void {
if (this.hasSearchResults) {
this.currentPage = 1;
this.loadTransactions();
}
}
onExportToExcel(): void {
if (!this.searchForm.valid) {
this.toastr.error('Please fill in all required fields');
return;
}
this.loading = true;
const filter: IAdminTransactionCreatedDateFilter = {
pageNumber: 1,
pageSize: this.totalRecords,
startDate: this.searchForm.get('startDate')?.value,
endDate: this.searchForm.get('endDate')?.value,
searchQuery: this.searchForm.get('searchQuery')?.value
};
this.transactionService.exportToExcel(filter).subscribe({
next: (blob: Blob) => {
const fileName = `Transactions_${new Date().toISOString()}.xlsx`;
saveAs(blob, fileName);
this.toastr.success('Export completed successfully');
this.loading = false;
},
error: (error) => {
this.loading = false;
this.toastr.error('Error exporting to Excel');
}
});
}
get formControls() {
return this.searchForm.controls;
}
}
admin-created-date-transactionsponent.html:
<div class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1 class="m-0">Admin Dashboard: {{ pageTitle }}</h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item">
<a [routerLink]="['/admin-dashboard']">Dashboard</a>
</li>
<li class="breadcrumb-item active">{{ pageTitle }}</li>
</ol>
</div>
</div>
</div>
</div>
<section class="content">
<div class="container-fluid">
<!-- Search Form -->
<form [formGroup]="searchForm" (ngSubmit)="onSearch()">
<div class="row mb-3">
<div class="col-md-3">
<label for="startDate">Start Date:<span style="color: red">*</span></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"
><i class="far fa-calendar-alt"></i
></span>
</div>
<input
type="text"
placeholder="DD-MM-YYYY"
class="form-control"
formControlName="startDate"
bsDatepicker
[bsConfig]="bsConfig"
[maxDate]="maxDate"
[minDate]="minDate"
[readonly]="true"
placement="bottom"
>
</div>
<div *ngIf="formControls['startDate'].touched && formControls['startDate'].errors" class="text-danger">
<small *ngIf="formControls['startDate'].errors?.['required']">Start date is required</small>
<small *ngIf="formControls['startDate'].errors?.['tooOld']">Date cannot be more than one year old</small>
</div>
</div>
<div class="col-md-3">
<label for="endDate">End Date:<span style="color: red">*</span></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"
><i class="far fa-calendar-alt"></i
></span>
</div>
<input
type="text"
placeholder="DD-MM-YYYY"
class="form-control"
formControlName="endDate"
bsDatepicker
[bsConfig]="bsConfig"
[maxDate]="maxDate"
[minDate]="minDate"
[readonly]="true"
placement="bottom"
>
</div>
<div *ngIf="formControls['endDate'].touched && formControls['endDate'].errors" class="text-danger">
<small *ngIf="formControls['endDate'].errors?.['required']">End date is required</small>
<small *ngIf="formControls['endDate'].errors?.['dateRange']">End date must be after start date</small>
</div>
</div>
<div class="col-md-4">
<label class="form-label">Search Query</label>
<input
type="text"
class="form-control"
formControlName="searchQuery"
placeholder="Enter search term..."
>
</div>
<div class="col-md-2">
<label class="form-label">Items Per Page</label>
<select class="form-control" formControlName="pageSize" (change)="onPageSizeChange()">
<option *ngFor="let size of pageSizeOptions" [value]="size">{{size}}</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group d-flex justify-content-end align-items-end h-100">
<button type="submit" class="btn btn-primary mr-2" [disabled]="!searchForm.valid || loading">
<i class="fas fa-search mr-1"></i>
{{ loading ? 'Searching...' : 'Search' }}
</button>
<button type="button" class="btn btn-success"
[disabled]="!searchForm.valid || !hasSearchResults || loading"
(click)="onExportToExcel()">
<i class="fas fa-file-excel mr-1"></i>
{{ loading ? 'Exporting...' : 'Export to Excel' }}
</button>
</div>
</div>
</form>
<div class="row">
<div class="col-sm-12 col-xs-12 col-12">
<div class="card card-danger">
<div class="card-header">
<h3 class="card-title">{{ pageTitle }} List</h3>
<div class="card-tools">
<button
type="button"
class="btn btn-tool"
data-card-widget="collapse"
>
<i class="fas fa-minus"></i>
</button>
</div>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>#</th>
<th>Account No</th>
<th>Amount</th>
<th>Source</th>
<th>Transaction Type</th>
<th>Description</th>
<th>Create Date</th>
<th>Channel Name</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let transaction of transactions; let i = index">
<td>{{((currentPage - 1) * pageSize) + i + 1}}</td>
<td>{{transaction.acctNo}}</td>
<td>{{transaction.amount | currency : " N" || "N/A" }}</td>
<td>{{transaction.source}}</td>
<td>{{transaction.transactionType}}</td>
<td>{{transaction.description}}</td>
<td>{{transaction.createDt | date : "dd-MMM-yyyy" || "N/A" }}</td>
<td>{{transaction.channelName}}</td>
</tr>
<tr *ngIf="!transactions?.length && hasSearchResults">
<td colspan="9" class="text-center">No transactions found</td>
</tr>
<tr *ngIf="!hasSearchResults">
<td colspan="9" class="text-center">Please select date range and search</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Pagination -->
<div class="card-footer clearfix" *ngIf="totalRecords > 0">
<div class="row align-items-center">
<!-- Pagination -->
<div class="d-flex justify-content-center mt-3">
<!-- <pagination
[totalItems]="totalRecords"
[itemsPerPage]="pageSize"
[maxSize]="7"
[(ngModel)]="currentPage"
(pageChanged)="onPageChange($event)"
></pagination> -->
<pagination
[totalItems]="totalRecords"
[itemsPerPage]="pageSize"
[maxSize]="5"
[(ngModel)]="currentPage"
[boundaryLinks]="true"
[directionLinks]="true"
[rotate]="true"
[firstText]="'First'"
[lastText]="'Last'"
[previousText]="'Previous'"
[nextText]="'Next'"
(pageChanged)="onPageChange($event)"
class="mb-0"
></pagination>
</div>
<div class="col-md-6 text-right">
Showing {{((currentPage - 1) * pageSize) + 1}} to {{Math.min(currentPage * pageSize, totalRecords)}} of {{totalRecords}} entries
</div>
</div>
</div>
</div>
</div>
</div>
</section>
Module:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { ReactiveFormsModule, FormsModule } from '@angular/forms';
// NGX-Bootstrap
import { PaginationModule } from 'ngx-bootstrap/pagination';
import { NgxPaginationModule, PaginatePipe } from 'ngx-pagination'; //Imports NgxPaginationModule
import { ModalModule, BsModalService } from 'ngx-bootstrap/modal';
import { BsDatepickerModule, BsDatepickerConfig, BsDaterangepickerConfig } from 'ngx-bootstrap/datepicker';
import { OrderModule } from 'ngx-order-pipe';
import { AlertModule,AlertConfig } from 'ngx-bootstrap/alert';
import { PopoverModule, PopoverConfig } from 'ngx-bootstrap/popover';
import { ProgressbarModule,ProgressbarConfig } from 'ngx-bootstrap/progressbar';
ToStatusPipe
@NgModule({
providers: [
AlertConfig,
BsDatepickerConfig,
BsDaterangepickerConfig,
BsModalService
],
declarations: [
],
imports: [
NgxPaginationModule,
AlertModule,
PaginationModule.forRoot(),
CommonModule,
RouterModule,
ReactiveFormsModule,
FormsModule,
BsDatepickerModule.forRoot(),
ModalModule.forRoot(),
OrderModule
],
exports: [
CommonModule,
NgxPaginationModule,
PaginationModule,
OrderModule,
ReactiveFormsModule,
BsDatepickerModule
],
})
export class SharedModule { }