Refactor transaction logs UI and logic for clarity
Redesigned the transaction logs component to use a reactive form for date filtering, simplified the table and search logic, and improved pagination and export functionality. Updated SCSS for cleaner date input styling and adjusted i18n keys for new UI labels.mazdak/UX-transactionLogs
parent
9039567896
commit
cdec4e63ec
@ -1,139 +1,29 @@
|
||||
// Date filter section
|
||||
.date-filter-section {
|
||||
transition: all 0.3s ease;
|
||||
|
||||
.date-input-group {
|
||||
.date-input-wrapper {
|
||||
position: relative;
|
||||
|
||||
.form-control {
|
||||
padding-right: 2.5rem;
|
||||
}
|
||||
input[type="date"] {
|
||||
cursor: pointer;
|
||||
padding-right: 35px;
|
||||
|
||||
.date-icon {
|
||||
&::-webkit-calendar-picker-indicator {
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #6c757d;
|
||||
}
|
||||
}
|
||||
|
||||
.quick-filter-btn {
|
||||
&.active {
|
||||
background-color: var(--bs-primary);
|
||||
color: white;
|
||||
border-color: var(--bs-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Table styling
|
||||
.table-responsive {
|
||||
min-height: 400px;
|
||||
|
||||
table {
|
||||
font-size: 0.875rem;
|
||||
|
||||
th {
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
td {
|
||||
vertical-align: middle;
|
||||
|
||||
code {
|
||||
font-size: 0.75rem;
|
||||
background-color: #f8f9fa;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loading spinner
|
||||
.spinner-border {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
// Search box
|
||||
.search-box {
|
||||
position: relative;
|
||||
min-width: 200px;
|
||||
|
||||
.search-icon {
|
||||
.calendar-icon {
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #6c757d;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
padding-right: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Active filter badge
|
||||
.active-filter-badge {
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile responsive adjustments
|
||||
@media (max-width: 768px) {
|
||||
.date-filter-section {
|
||||
.btn-group {
|
||||
width: 100%;
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.375rem 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
overflow-x: auto;
|
||||
|
||||
table {
|
||||
min-width: 1200px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-box {
|
||||
min-width: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
// Dark mode support (if needed)
|
||||
[data-bs-theme="dark"] {
|
||||
.table-light {
|
||||
background-color: #2d333b !important;
|
||||
color: #adb5bd;
|
||||
}
|
||||
|
||||
.search-box .search-icon {
|
||||
color: #adb5bd;
|
||||
color: #6c757d;
|
||||
}
|
||||
}
|
||||
@ -1,340 +1,171 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CommonModule, DatePipe } from '@angular/common';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ExcelExportService } from '../shared/services/excel-export.service';
|
||||
import { TRANSACTION_LOGS_FILE_NAME } from '../utils/app.constants';
|
||||
import { toDateAfterFromDateValidator, TRANSACTION_LOGS_FILE_NAME } from '../utils/app.constants';
|
||||
import { pageSizeOptions } from '../utils/app.constants';
|
||||
import { NgSelectModule } from '@ng-select/ng-select';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { TableFilterPipe } from '../shared/pipes/table-filter.pipe';
|
||||
import { TransactionLog } from "../models/user";
|
||||
import { URIKey } from '../utils/uri-enums';
|
||||
import { HttpParams } from '@angular/common/http';
|
||||
import { HttpURIService } from '../app.http.uri.service';
|
||||
import { formatDate } from '@angular/common';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil, debounceTime } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'app-transaction-logs',
|
||||
templateUrl: './transaction-logs.component.html',
|
||||
styleUrls: ['./transaction-logs.component.scss'],
|
||||
imports: [CommonModule, TranslateModule, NgSelectModule, FormsModule, ReactiveFormsModule, TableFilterPipe]
|
||||
})
|
||||
export class TransactionLogsComponent implements OnInit, OnDestroy {
|
||||
currentPage: number = 1;
|
||||
totalCount: number = 0;
|
||||
renewalDataExpanded: boolean = true;
|
||||
imports: [
|
||||
TranslateModule,
|
||||
FormsModule,
|
||||
NgSelectModule,
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
TableFilterPipe
|
||||
],
|
||||
providers: [DatePipe]})
|
||||
export class TransactionLogsComponent implements OnInit {
|
||||
logsSearchForm!: FormGroup;
|
||||
|
||||
pageSizeOptions = pageSizeOptions;
|
||||
itemsPerPage: number = 10;
|
||||
transactionLog: TransactionLog[] = [];
|
||||
isLoading = false;
|
||||
errorMessage: string = '';
|
||||
searchText: string = '';
|
||||
allItems: TransactionLog[] = [];
|
||||
transactionDataExpanded: boolean = true;
|
||||
itemsPerPage = 10;
|
||||
currentPage = 1;
|
||||
maxDate: string;
|
||||
searchText = '';
|
||||
|
||||
// Date range properties
|
||||
fromDate: string = '';
|
||||
toDate: string = '';
|
||||
maxDate: string = '';
|
||||
showDateFilters: boolean = false;
|
||||
logsDataExpanded = true;
|
||||
isLoading = false;
|
||||
|
||||
// Search subject for debouncing
|
||||
private searchSubject = new Subject<string>();
|
||||
private destroy$ = new Subject<void>();
|
||||
/** DATA LAYERS (do not mix these) */
|
||||
allItems: TransactionLog[] = []; // raw API data
|
||||
filteredItems: TransactionLog[] = []; // after search
|
||||
pagedItems: TransactionLog[] = []; // table view
|
||||
|
||||
constructor(
|
||||
private excelExportService: ExcelExportService,
|
||||
private fb: FormBuilder,
|
||||
private httpService: HttpURIService,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Set max date to today
|
||||
this.maxDate = formatDate(new Date(), 'yyyy-MM-dd', 'en-US');
|
||||
|
||||
// Set default date range (last 7 days)
|
||||
const today = new Date();
|
||||
const lastWeek = new Date();
|
||||
lastWeek.setDate(today.getDate() - 7);
|
||||
|
||||
this.fromDate = formatDate(lastWeek, 'yyyy-MM-dd', 'en-US');
|
||||
this.toDate = formatDate(today, 'yyyy-MM-dd', 'en-US');
|
||||
|
||||
// Set up search debouncing
|
||||
this.setupSearchDebounce();
|
||||
|
||||
this.loadTransactionLogs();
|
||||
private datePipe: DatePipe,
|
||||
private excelExportService: ExcelExportService,
|
||||
) {
|
||||
this.maxDate = new Date().toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
this.searchSubject.complete();
|
||||
ngOnInit(): void {
|
||||
this.logsSearchForm = this.fb.group(
|
||||
{
|
||||
fromDate: ['', Validators.required],
|
||||
toDate: ['', Validators.required],
|
||||
},
|
||||
{ validators: toDateAfterFromDateValidator },
|
||||
);
|
||||
}
|
||||
|
||||
private setupSearchDebounce(): void {
|
||||
this.searchSubject.pipe(
|
||||
takeUntil(this.destroy$),
|
||||
debounceTime(300)
|
||||
).subscribe((searchValue: string) => {
|
||||
this.searchText = searchValue;
|
||||
this.currentPage = 1;
|
||||
this.loadTransactionLogs();
|
||||
});
|
||||
}
|
||||
getlogsData(): void {
|
||||
if (this.logsSearchForm.invalid) return;
|
||||
|
||||
loadTransactionLogs(): void {
|
||||
this.isLoading = true;
|
||||
this.errorMessage = '';
|
||||
|
||||
// Validate date range before making API call
|
||||
if (!this.validateDateRange()) {
|
||||
this.isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Build query parameters
|
||||
let params = new HttpParams();
|
||||
|
||||
// Add pagination parameters
|
||||
params = params.set('page', this.currentPage.toString());
|
||||
params = params.set('limit', this.itemsPerPage.toString());
|
||||
|
||||
// Add date filters if provided
|
||||
if (this.fromDate) {
|
||||
params = params.set('fromDate', this.fromDate);
|
||||
}
|
||||
const { fromDate, toDate } = this.logsSearchForm.value;
|
||||
|
||||
if (this.toDate) {
|
||||
params = params.set('toDate', this.toDate);
|
||||
}
|
||||
|
||||
// Add search filter if provided
|
||||
if (this.searchText && this.searchText.trim() !== '') {
|
||||
params = params.set('search', this.searchText.trim());
|
||||
}
|
||||
const params = new HttpParams()
|
||||
.set('fromDate', this.datePipe.transform(fromDate, 'dd-MM-yyyy')!)
|
||||
.set('toDate', this.datePipe.transform(toDate, 'dd-MM-yyyy')!);
|
||||
|
||||
this.httpService
|
||||
.requestGET<any>(URIKey.TRANSACTION_LOGS, params)
|
||||
.requestGET<TransactionLog[]>(URIKey.TRANSACTION_LOGS, params)
|
||||
.subscribe({
|
||||
next: (res) => {
|
||||
const logs = Array.isArray(res) ? res : res?.data;
|
||||
this.transactionLog = logs ?? [];
|
||||
this.allItems = [...this.transactionLog];
|
||||
|
||||
// Get total count from response
|
||||
if (res?.total !== undefined) {
|
||||
this.totalCount = res.total;
|
||||
} else if (res?.pagination?.total !== undefined) {
|
||||
this.totalCount = res.pagination.total;
|
||||
} else if (res?.meta?.total !== undefined) {
|
||||
this.totalCount = res.meta.total;
|
||||
} else {
|
||||
this.totalCount = this.transactionLog.length;
|
||||
}
|
||||
|
||||
next: (response) => {
|
||||
this.allItems = response ?? [];
|
||||
this.filteredItems = [...this.allItems];
|
||||
this.currentPage = 1;
|
||||
this.updatePagedItems();
|
||||
this.isLoading = false;
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Error fetching transaction logs:', err);
|
||||
this.errorMessage = err.message || 'Failed to load transaction logs. Please try again.';
|
||||
this.transactionLog = [];
|
||||
error: () => {
|
||||
this.allItems = [];
|
||||
this.totalCount = 0;
|
||||
this.filteredItems = [];
|
||||
this.pagedItems = [];
|
||||
this.isLoading = false;
|
||||
},
|
||||
complete: () => {
|
||||
this.isLoading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private validateDateRange(): boolean {
|
||||
if (this.fromDate && this.toDate) {
|
||||
const from = new Date(this.fromDate);
|
||||
const to = new Date(this.toDate);
|
||||
|
||||
if (from > to) {
|
||||
this.errorMessage = 'From date cannot be after To date';
|
||||
return false;
|
||||
}
|
||||
|
||||
// Optional: Validate date range is not too wide
|
||||
const diffTime = Math.abs(to.getTime() - from.getTime());
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
/** SEARCH — filters DATA, not DOM */
|
||||
applySearch(): void {
|
||||
const value = this.searchText.toLowerCase().trim();
|
||||
|
||||
if (diffDays > 365) {
|
||||
this.errorMessage = 'Date range cannot exceed 1 year';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Date filter change handler
|
||||
onDateFilterChange(): void {
|
||||
this.currentPage = 1;
|
||||
this.loadTransactionLogs();
|
||||
}
|
||||
|
||||
// Clear date filters
|
||||
clearDateFilters(): void {
|
||||
this.fromDate = '';
|
||||
this.toDate = '';
|
||||
this.currentPage = 1;
|
||||
this.loadTransactionLogs();
|
||||
}
|
||||
|
||||
// Toggle date filter visibility
|
||||
toggleDateFilters(): void {
|
||||
this.showDateFilters = !this.showDateFilters;
|
||||
}
|
||||
|
||||
// Apply default date range (Last 7 days)
|
||||
applyLast7Days(): void {
|
||||
const today = new Date();
|
||||
const lastWeek = new Date();
|
||||
lastWeek.setDate(today.getDate() - 7);
|
||||
|
||||
this.fromDate = formatDate(lastWeek, 'yyyy-MM-dd', 'en-US');
|
||||
this.toDate = formatDate(today, 'yyyy-MM-dd', 'en-US');
|
||||
this.onDateFilterChange();
|
||||
}
|
||||
|
||||
// Apply default date range (Last 30 days)
|
||||
applyLast30Days(): void {
|
||||
const today = new Date();
|
||||
const lastMonth = new Date();
|
||||
lastMonth.setDate(today.getDate() - 30);
|
||||
|
||||
this.fromDate = formatDate(lastMonth, 'yyyy-MM-dd', 'en-US');
|
||||
this.toDate = formatDate(today, 'yyyy-MM-dd', 'en-US');
|
||||
this.onDateFilterChange();
|
||||
}
|
||||
|
||||
// Apply default date range (This month)
|
||||
applyThisMonth(): void {
|
||||
const today = new Date();
|
||||
const firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
|
||||
this.fromDate = formatDate(firstDay, 'yyyy-MM-dd', 'en-US');
|
||||
this.toDate = formatDate(today, 'yyyy-MM-dd', 'en-US');
|
||||
this.onDateFilterChange();
|
||||
}
|
||||
|
||||
// Apply date range (Last 3 months)
|
||||
applyLast3Months(): void {
|
||||
const today = new Date();
|
||||
const threeMonthsAgo = new Date();
|
||||
threeMonthsAgo.setMonth(today.getMonth() - 3);
|
||||
|
||||
this.fromDate = formatDate(threeMonthsAgo, 'yyyy-MM-dd', 'en-US');
|
||||
this.toDate = formatDate(today, 'yyyy-MM-dd', 'en-US');
|
||||
this.onDateFilterChange();
|
||||
}
|
||||
|
||||
toggleTableCard(): void {
|
||||
this.transactionDataExpanded = !this.transactionDataExpanded;
|
||||
if (!value) {
|
||||
this.filteredItems = [...this.allItems];
|
||||
} else {
|
||||
this.filteredItems = this.allItems.filter((item) =>
|
||||
[
|
||||
'logId',
|
||||
'porOrgacode',
|
||||
'transactionID',
|
||||
'drMbmbkmsnumber',
|
||||
'crMbmbkmsnumber',
|
||||
'crPcaglacode',
|
||||
'drPcaGlacode',
|
||||
'amount',
|
||||
'paymentMode',
|
||||
'ppmPymdcode',
|
||||
'sgtGntrdate',
|
||||
'channelCode',
|
||||
'createdAt',
|
||||
'transactionUri',
|
||||
'transactionCode',
|
||||
].some((key) =>
|
||||
String((item as any)[key] ?? '')
|
||||
.toLowerCase()
|
||||
.includes(value),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
itemsPerPageChanged(): void {
|
||||
this.currentPage = 1;
|
||||
this.loadTransactionLogs();
|
||||
this.updatePagedItems();
|
||||
}
|
||||
|
||||
onSearch(value: string): void {
|
||||
this.searchSubject.next(value);
|
||||
updatePagedItems(): void {
|
||||
const start = (this.currentPage - 1) * this.itemsPerPage;
|
||||
const end = start + this.itemsPerPage;
|
||||
this.pagedItems = this.filteredItems.slice(start, end);
|
||||
}
|
||||
|
||||
totalPages(): number {
|
||||
if (this.totalCount === 0) return 1;
|
||||
return Math.ceil(this.totalCount / this.itemsPerPage);
|
||||
}
|
||||
return Math.ceil(this.filteredItems.length / this.itemsPerPage);
|
||||
}
|
||||
|
||||
previousPage(): void {
|
||||
if (this.currentPage > 1) {
|
||||
this.currentPage--;
|
||||
this.loadTransactionLogs();
|
||||
this.updatePagedItems();
|
||||
}
|
||||
}
|
||||
|
||||
nextPage(): void {
|
||||
if (this.currentPage < this.totalPages()) {
|
||||
this.currentPage++;
|
||||
this.loadTransactionLogs();
|
||||
}
|
||||
}
|
||||
|
||||
exportDataInExcel(): void {
|
||||
// Build export parameters
|
||||
let params = new HttpParams();
|
||||
|
||||
// Include date filters in export if they are set
|
||||
if (this.fromDate) {
|
||||
params = params.set('fromDate', this.fromDate);
|
||||
}
|
||||
|
||||
if (this.toDate) {
|
||||
params = params.set('toDate', this.toDate);
|
||||
}
|
||||
|
||||
if (this.searchText && this.searchText.trim() !== '') {
|
||||
params = params.set('search', this.searchText.trim());
|
||||
}
|
||||
|
||||
// For large exports, you might want to fetch all data without pagination
|
||||
params = params.set('limit', '10000'); // Adjust as needed
|
||||
|
||||
this.httpService
|
||||
.requestGET<any>(URIKey.TRANSACTION_LOGS, params)
|
||||
.subscribe({
|
||||
next: (res) => {
|
||||
const logs = Array.isArray(res) ? res : res?.data;
|
||||
const dataToExport = logs ?? [];
|
||||
|
||||
if (dataToExport.length === 0) {
|
||||
this.errorMessage = 'No data to export';
|
||||
return;
|
||||
this.updatePagedItems();
|
||||
}
|
||||
|
||||
// Generate filename with date range if applicable
|
||||
let fileName = TRANSACTION_LOGS_FILE_NAME;
|
||||
if (this.fromDate || this.toDate) {
|
||||
const from = this.fromDate ? formatDate(new Date(this.fromDate), 'dd-MMM-yyyy', 'en-US') : 'All';
|
||||
const to = this.toDate ? formatDate(new Date(this.toDate), 'dd-MMM-yyyy', 'en-US') : 'All';
|
||||
fileName = `TransactionLogs_${from}_to_${to}`;
|
||||
}
|
||||
|
||||
this.excelExportService.exportExcel(dataToExport, fileName);
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Error exporting data:', err);
|
||||
this.errorMessage = 'Failed to export data. Please try again.';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Get filtered items for display (used in template)
|
||||
get filteredItems(): TransactionLog[] {
|
||||
return this.allItems;
|
||||
itemsPerPageChanged(): void {
|
||||
this.currentPage = 1;
|
||||
this.updatePagedItems();
|
||||
}
|
||||
|
||||
// Format date for display
|
||||
formatDateDisplay(dateString: string): string {
|
||||
if (!dateString) return '';
|
||||
return formatDate(new Date(dateString), 'MMM dd, yyyy', 'en-US');
|
||||
toggleTableCard(): void {
|
||||
this.logsDataExpanded = !this.logsDataExpanded;
|
||||
}
|
||||
|
||||
// Check if date filter is active
|
||||
get isDateFilterActive(): boolean {
|
||||
return !!this.fromDate || !!this.toDate;
|
||||
exportDataInExcel(): void {
|
||||
this.excelExportService.exportExcel(
|
||||
this.allItems,
|
||||
TRANSACTION_LOGS_FILE_NAME,
|
||||
);
|
||||
}
|
||||
|
||||
get pagedItems(): TransactionLog[] {
|
||||
const start = (this.currentPage - 1) * this.itemsPerPage;
|
||||
return this.allItems.slice(start, start + this.itemsPerPage);
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue