Merge branch 'mazdak/UX-transactionLogs' into PRE-PRODUCTION-2026
commit
16be190777
@ -1,139 +1,29 @@
|
||||
// Date filter section
|
||||
.date-filter-section {
|
||||
transition: all 0.3s ease;
|
||||
.date-input-wrapper {
|
||||
position: relative;
|
||||
|
||||
.date-input-group {
|
||||
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[] = [];
|
||||
itemsPerPage = 10;
|
||||
currentPage = 1;
|
||||
maxDate: string;
|
||||
searchText = '';
|
||||
|
||||
logsDataExpanded = true;
|
||||
isLoading = false;
|
||||
errorMessage: string = '';
|
||||
searchText: string = '';
|
||||
allItems: TransactionLog[] = [];
|
||||
transactionDataExpanded: boolean = true;
|
||||
|
||||
// Date range properties
|
||||
fromDate: string = '';
|
||||
toDate: string = '';
|
||||
maxDate: string = '';
|
||||
showDateFilters: boolean = 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);
|
||||
}
|
||||
|
||||
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 { fromDate, toDate } = this.logsSearchForm.value;
|
||||
|
||||
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));
|
||||
|
||||
if (diffDays > 365) {
|
||||
this.errorMessage = 'Date range cannot exceed 1 year';
|
||||
return false;
|
||||
}
|
||||
/** SEARCH — filters DATA, not DOM */
|
||||
applySearch(): void {
|
||||
const value = this.searchText.toLowerCase().trim();
|
||||
|
||||
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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Date filter change handler
|
||||
onDateFilterChange(): void {
|
||||
this.currentPage = 1;
|
||||
this.loadTransactionLogs();
|
||||
this.updatePagedItems();
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
itemsPerPageChanged(): void {
|
||||
this.currentPage = 1;
|
||||
this.loadTransactionLogs();
|
||||
}
|
||||
|
||||
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();
|
||||
this.updatePagedItems();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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