Add date range filtering to transaction logs

Introduces a date filter section with quick-select buttons (last 7 days, last 30 days, this month), clear filter, and active filter badge to the transaction logs component. Implements debounced search, date range validation, and updates export to include date filters. Enhances UI/UX with new SCSS styles and adds relevant i18n keys for both English and Arabic.
mazdak/UX-2367
Naeem Ullah 5 days ago
parent 0929acd502
commit 9821ed4d9e

@ -21,6 +21,16 @@
{{ "transactionLogs" | translate }} {{ "transactionLogs" | translate }}
<div class="d-flex align-items-center gap-2"> <div class="d-flex align-items-center gap-2">
<!-- Date Filter Toggle Button -->
<button
class="btn btn-sm btn-outline-info"
(click)="toggleDateFilters()"
[title]="(showDateFilters ? 'hideDateFilters' : 'showDateFilters') | translate"
>
<i class="fas fa-calendar-alt"></i>
<span class="d-none d-md-inline ms-1">{{ "dateFilter" | translate }}</span>
</button>
<div class="search-box"> <div class="search-box">
<input <input
type="text" type="text"
@ -55,6 +65,83 @@
</div> </div>
</div> </div>
<!-- Date Filter Section -->
<div class="card-body border-bottom bg-light" *ngIf="showDateFilters">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label small">{{ "fromDate" | translate }}</label>
<input
type="date"
class="form-control form-control-sm"
[(ngModel)]="fromDate"
[max]="maxDate"
(change)="onDateFilterChange()"
[disabled]="isLoading"
/>
</div>
<div class="col-md-3">
<label class="form-label small">{{ "toDate" | translate }}</label>
<input
type="date"
class="form-control form-control-sm"
[(ngModel)]="toDate"
[max]="maxDate"
(change)="onDateFilterChange()"
[disabled]="isLoading"
/>
</div>
<div class="col-md-6">
<div class="d-flex flex-wrap gap-2 align-items-end h-100">
<!-- Quick Date Filters -->
<div class="btn-group btn-group-sm">
<button
class="btn btn-outline-primary"
(click)="applyLast7Days()"
[disabled]="isLoading"
>
{{ "last7Days" | translate }}
</button>
<button
class="btn btn-outline-primary"
(click)="applyLast30Days()"
[disabled]="isLoading"
>
{{ "last30Days" | translate }}
</button>
<button
class="btn btn-outline-primary"
(click)="applyThisMonth()"
[disabled]="isLoading"
>
{{ "thisMonth" | translate }}
</button>
</div>
<!-- Clear Filter Button -->
<button
class="btn btn-sm btn-outline-danger"
(click)="clearDateFilters()"
[disabled]="isLoading || (!fromDate && !toDate)"
>
<i class="fas fa-times"></i>
{{ "clearFilter" | translate }}
</button>
</div>
</div>
</div>
<!-- Active Filter Badge -->
<div class="mt-2" *ngIf="fromDate || toDate">
<span class="badge bg-info">
<i class="fas fa-filter me-1"></i>
{{ "activeFilter" | translate }}:
<span *ngIf="fromDate">{{ "from" | translate }} {{ fromDate | date }}</span>
<span *ngIf="fromDate && toDate"> {{ "to" | translate }} </span>
<span *ngIf="toDate">{{ toDate | date }}</span>
</span>
</div>
</div>
<div <div
class="card-body" class="card-body"
*ngIf="transactionDataExpanded; else collapsedTable" *ngIf="transactionDataExpanded; else collapsedTable"
@ -65,6 +152,9 @@
<span class="visually-hidden">Loading...</span> <span class="visually-hidden">Loading...</span>
</div> </div>
<p class="text-muted mt-2">{{ "loadingTransactionLogs" | translate }}</p> <p class="text-muted mt-2">{{ "loadingTransactionLogs" | translate }}</p>
<small *ngIf="fromDate || toDate" class="text-info">
{{ "filteringByDate" | translate }}
</small>
</div> </div>
<!-- Error Message --> <!-- Error Message -->
@ -77,6 +167,9 @@
<div *ngIf="!isLoading && !errorMessage && transactionLog.length === 0" class="text-center py-5 text-muted"> <div *ngIf="!isLoading && !errorMessage && transactionLog.length === 0" class="text-center py-5 text-muted">
<i class="fas fa-database fa-3x mb-3 opacity-50"></i> <i class="fas fa-database fa-3x mb-3 opacity-50"></i>
<h5>{{ "noTransactionLogsFound" | translate }}</h5> <h5>{{ "noTransactionLogsFound" | translate }}</h5>
<p *ngIf="fromDate || toDate" class="small">
{{ "tryAdjustingFilters" | translate }}
</p>
</div> </div>
<!-- Data Table --> <!-- Data Table -->
@ -167,6 +260,9 @@
<span class="d-none d-md-inline"> <span class="d-none d-md-inline">
({{ totalCount }} {{ "totalItems" | translate }}) ({{ totalCount }} {{ "totalItems" | translate }})
</span> </span>
<span *ngIf="fromDate || toDate" class="badge bg-info ms-2">
<i class="fas fa-filter"></i>
</span>
</div> </div>
<!-- Pagination buttons --> <!-- Pagination buttons -->

@ -1,125 +1,139 @@
:host { // Date filter section
display: block; .date-filter-section {
} transition: all 0.3s ease;
.card-header {
background-color: #f8f9fa;
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
}
.search-box { .date-input-group {
position: relative; position: relative;
min-width: 200px;
.form-control { .form-control {
padding-left: 2.5rem; padding-right: 2.5rem;
padding-right: 1rem;
} }
.search-icon { .date-icon {
position: absolute; position: absolute;
left: 1rem; right: 0.75rem;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
color: #6c757d; color: #6c757d;
pointer-events: none;
} }
} }
.table { .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 { th {
font-weight: 600; font-weight: 600;
background-color: #f8f9fa;
border-bottom: 2px solid #dee2e6;
white-space: nowrap; white-space: nowrap;
background-color: #f8f9fa;
} }
td { td {
vertical-align: middle; vertical-align: middle;
}
}
.btn-group-sm > .btn { code {
padding: 0.25rem 0.5rem; font-size: 0.75rem;
font-size: 0.875rem; background-color: #f8f9fa;
padding: 0.2rem 0.4rem;
border-radius: 0.25rem;
} }
.badge { .badge {
font-size: 0.75em; font-size: 0.7rem;
padding: 0.25rem 0.5rem;
}
} }
.alert-info {
background-color: #e7f3ff;
border-color: #b3d7ff;
color: #004085;
.btn-close {
padding: 0.5rem;
} }
} }
// Loading spinner
.spinner-border { .spinner-border {
width: 3rem; width: 3rem;
height: 3rem; height: 3rem;
} }
// Responsive adjustments // Search box
@media (max-width: 768px) {
.card-header .d-flex {
flex-direction: column;
align-items: stretch !important;
gap: 0.75rem !important;
}
.search-box { .search-box {
min-width: 100%; position: relative;
min-width: 200px;
.search-icon {
position: absolute;
right: 0.75rem;
top: 50%;
transform: translateY(-50%);
color: #6c757d;
pointer-events: none;
} }
.table-responsive { .form-control {
font-size: 0.875rem; padding-right: 2.5rem;
}
} }
.d-flex.justify-content-between { // Active filter badge
flex-direction: column; .active-filter-badge {
gap: 1rem; animation: fadeIn 0.3s ease;
}
> div { @keyframes fadeIn {
width: 100%; from {
justify-content: center !important; opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
} }
} }
// Mobile responsive adjustments
@media (max-width: 768px) {
.date-filter-section {
.btn-group { .btn-group {
width: 100%; width: 100%;
.btn { .btn {
flex: 1; flex: 1;
font-size: 0.75rem;
padding: 0.375rem 0.5rem;
} }
} }
} }
// Loading overlay .table-responsive {
.text-center.py-5 { overflow-x: auto;
min-height: 200px;
display: flex; table {
flex-direction: column; min-width: 1200px;
justify-content: center; }
align-items: center;
} }
// Hover effects .search-box {
.table-hover tbody tr:hover { min-width: 150px;
background-color: rgba(0, 123, 255, 0.05); }
} }
// Date filter styles // Dark mode support (if needed)
.form-control-sm { [data-bs-theme="dark"] {
min-width: 120px; .table-light {
background-color: #2d333b !important;
color: #adb5bd;
} }
// Active state for filter buttons .search-box .search-icon {
.btn-outline-primary.active { color: #adb5bd;
background-color: #0d6efd; }
color: white;
border-color: #0d6efd;
} }

@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { ExcelExportService } from '../shared/services/excel-export.service'; import { ExcelExportService } from '../shared/services/excel-export.service';
@ -12,6 +12,8 @@ import { URIKey } from '../utils/uri-enums';
import { HttpParams } from '@angular/common/http'; import { HttpParams } from '@angular/common/http';
import { HttpURIService } from '../app.http.uri.service'; import { HttpURIService } from '../app.http.uri.service';
import { formatDate } from '@angular/common'; import { formatDate } from '@angular/common';
import { Subject } from 'rxjs';
import { takeUntil, debounceTime } from 'rxjs/operators';
@Component({ @Component({
selector: 'app-transaction-logs', selector: 'app-transaction-logs',
@ -19,7 +21,7 @@ import { formatDate } from '@angular/common';
styleUrls: ['./transaction-logs.component.scss'], styleUrls: ['./transaction-logs.component.scss'],
imports: [CommonModule, TranslateModule, NgSelectModule, FormsModule, ReactiveFormsModule, TableFilterPipe] imports: [CommonModule, TranslateModule, NgSelectModule, FormsModule, ReactiveFormsModule, TableFilterPipe]
}) })
export class TransactionLogsComponent implements OnInit { export class TransactionLogsComponent implements OnInit, OnDestroy {
currentPage: number = 1; currentPage: number = 1;
totalCount: number = 0; totalCount: number = 0;
renewalDataExpanded: boolean = true; renewalDataExpanded: boolean = true;
@ -39,6 +41,10 @@ export class TransactionLogsComponent implements OnInit {
maxDate: string = ''; maxDate: string = '';
showDateFilters: boolean = false; showDateFilters: boolean = false;
// Search subject for debouncing
private searchSubject = new Subject<string>();
private destroy$ = new Subject<void>();
constructor( constructor(
private excelExportService: ExcelExportService, private excelExportService: ExcelExportService,
private httpService: HttpURIService, private httpService: HttpURIService,
@ -48,21 +54,47 @@ export class TransactionLogsComponent implements OnInit {
// Set max date to today // Set max date to today
this.maxDate = formatDate(new Date(), 'yyyy-MM-dd', 'en-US'); this.maxDate = formatDate(new Date(), 'yyyy-MM-dd', 'en-US');
// Optionally set default date range (last 30 days) // Set default date range (last 7 days)
const today = new Date(); const today = new Date();
const lastMonth = new Date(); const lastWeek = new Date();
lastMonth.setDate(today.getDate() - 30); lastWeek.setDate(today.getDate() - 7);
this.fromDate = formatDate(lastMonth, 'yyyy-MM-dd', 'en-US'); this.fromDate = formatDate(lastWeek, 'yyyy-MM-dd', 'en-US');
this.toDate = formatDate(today, 'yyyy-MM-dd', 'en-US'); this.toDate = formatDate(today, 'yyyy-MM-dd', 'en-US');
// Set up search debouncing
this.setupSearchDebounce();
this.loadTransactionLogs();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
this.searchSubject.complete();
}
private setupSearchDebounce(): void {
this.searchSubject.pipe(
takeUntil(this.destroy$),
debounceTime(300)
).subscribe((searchValue: string) => {
this.searchText = searchValue;
this.currentPage = 1;
this.loadTransactionLogs(); this.loadTransactionLogs();
});
} }
loadTransactionLogs(): void { loadTransactionLogs(): void {
this.isLoading = true; this.isLoading = true;
this.errorMessage = ''; this.errorMessage = '';
// Validate date range before making API call
if (!this.validateDateRange()) {
this.isLoading = false;
return;
}
// Build query parameters // Build query parameters
let params = new HttpParams(); let params = new HttpParams();
@ -121,20 +153,31 @@ export class TransactionLogsComponent implements OnInit {
}); });
} }
// Date filter change handler private validateDateRange(): boolean {
onDateFilterChange(): void {
// Validate date range
if (this.fromDate && this.toDate) { if (this.fromDate && this.toDate) {
const from = new Date(this.fromDate); const from = new Date(this.fromDate);
const to = new Date(this.toDate); const to = new Date(this.toDate);
if (from > to) { if (from > to) {
this.errorMessage = 'From date cannot be after To date'; this.errorMessage = 'From date cannot be after To date';
return; 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;
}
}
return true;
} }
// Reset to first page and reload // Date filter change handler
onDateFilterChange(): void {
this.currentPage = 1; this.currentPage = 1;
this.loadTransactionLogs(); this.loadTransactionLogs();
} }
@ -184,6 +227,17 @@ export class TransactionLogsComponent implements OnInit {
this.onDateFilterChange(); 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 { toggleTableCard(): void {
this.transactionDataExpanded = !this.transactionDataExpanded; this.transactionDataExpanded = !this.transactionDataExpanded;
} }
@ -194,14 +248,7 @@ export class TransactionLogsComponent implements OnInit {
} }
onSearch(value: string): void { onSearch(value: string): void {
this.searchText = value; this.searchSubject.next(value);
this.currentPage = 1;
// Debounce search to avoid too many API calls
clearTimeout((this as any).searchTimeout);
(this as any).searchTimeout = setTimeout(() => {
this.loadTransactionLogs();
}, 300);
} }
totalPages(): number { totalPages(): number {
@ -220,8 +267,6 @@ export class TransactionLogsComponent implements OnInit {
const startIndex = (this.currentPage - 1) * this.itemsPerPage; const startIndex = (this.currentPage - 1) * this.itemsPerPage;
const endIndex = startIndex + this.itemsPerPage; const endIndex = startIndex + this.itemsPerPage;
// If using server-side pagination, use transactionLog directly
// If using client-side filtering with tableFilter pipe, this might not be needed
this.pagedItems = this.allItems.slice(startIndex, endIndex); this.pagedItems = this.allItems.slice(startIndex, endIndex);
} }
@ -233,13 +278,67 @@ export class TransactionLogsComponent implements OnInit {
} }
exportDataInExcel(): void { exportDataInExcel(): void {
// Export filtered data // Build export parameters
const dataToExport = this.allItems.length > 0 ? this.allItems : this.transactionLog; let params = new HttpParams();
this.excelExportService.exportExcel(dataToExport, TRANSACTION_LOGS_FILE_NAME);
// 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 filtered items for display (used in template)
get filteredItems(): TransactionLog[] { get filteredItems(): TransactionLog[] {
return this.allItems; return this.allItems;
} }
// Format date for display
formatDateDisplay(dateString: string): string {
if (!dateString) return '';
return formatDate(new Date(dateString), 'MMM dd, yyyy', 'en-US');
}
// Check if date filter is active
get isDateFilterActive(): boolean {
return !!this.fromDate || !!this.toDate;
}
} }

@ -295,5 +295,23 @@
"showTable": "عرض الجدول", "showTable": "عرض الجدول",
"collapse": "يطوي", "collapse": "يطوي",
"expand": "توسيع", "expand": "توسيع",
"entries": "إدخالات" "entries": "إدخالات",
"dateFilter": "تصفية التاريخ",
"fromDate": "من تاريخ",
"toDate": "إلى تاريخ",
"clearFilter": "مسح التصفية",
"activeFilter": "التصفية نشطة",
"from": "من",
"to": "إلى",
"last7Days": "آخر 7 أيام",
"last30Days": "آخر 30 يومًا",
"thisMonth": "هذا الشهر",
"last3Months": "آخر 3 أشهر",
"hideDateFilters": "إخفاء تصفية التاريخ",
"showDateFilters": "عرض تصفية التاريخ",
"filteringByDate": "جاري التصفية حسب نطاق التاريخ...",
"tryAdjustingFilters": "حاول تعديل تصفية التاريخ أو مصطلحات البحث",
"noDataInRange": "لم يتم العثور على معاملات في نطاق التاريخ المحدد",
"dateRangeError": "الرجاء تحديد نطاق تاريخ صالح",
"exportAllData": "تصدير جميع البيانات"
} }

@ -1,5 +1,4 @@
{ {
"logintoAccount": "Login to your account", "logintoAccount": "Login to your account",
"userName": "Username", "userName": "Username",
"userNamePlaceHolder": "Enter Username", "userNamePlaceHolder": "Enter Username",
@ -297,5 +296,21 @@
"tableCollapsed": "Table collapsed", "tableCollapsed": "Table collapsed",
"showTable": "Show Table", "showTable": "Show Table",
"collapse": "Collapse", "collapse": "Collapse",
"expand": "Expand" "expand": "Expand",
"dateFilter": "Date Filter",
"clearFilter": "Clear Filter",
"activeFilter": "Active Filter",
"from": "From",
"to": "to",
"last7Days": "Last 7 Days",
"last30Days": "Last 30 Days",
"thisMonth": "This Month",
"last3Months": "Last 3 Months",
"hideDateFilters": "Hide Date Filters",
"showDateFilters": "Show Date Filters",
"filteringByDate": "Filtering by date range...",
"tryAdjustingFilters": "Try adjusting your date filters or search terms",
"noDataInRange": "No transactions found in the selected date range",
"dateRangeError": "Please select a valid date range",
"exportAllData": "Export All Data"
} }
Loading…
Cancel
Save