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 }}
<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">
<input
type="text"
@ -55,6 +65,83 @@
</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
class="card-body"
*ngIf="transactionDataExpanded; else collapsedTable"
@ -65,6 +152,9 @@
<span class="visually-hidden">Loading...</span>
</div>
<p class="text-muted mt-2">{{ "loadingTransactionLogs" | translate }}</p>
<small *ngIf="fromDate || toDate" class="text-info">
{{ "filteringByDate" | translate }}
</small>
</div>
<!-- Error Message -->
@ -77,6 +167,9 @@
<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>
<h5>{{ "noTransactionLogsFound" | translate }}</h5>
<p *ngIf="fromDate || toDate" class="small">
{{ "tryAdjustingFilters" | translate }}
</p>
</div>
<!-- Data Table -->
@ -167,6 +260,9 @@
<span class="d-none d-md-inline">
({{ totalCount }} {{ "totalItems" | translate }})
</span>
<span *ngIf="fromDate || toDate" class="badge bg-info ms-2">
<i class="fas fa-filter"></i>
</span>
</div>
<!-- Pagination buttons -->

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

@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
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 { 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',
@ -19,7 +21,7 @@ import { formatDate } from '@angular/common';
styleUrls: ['./transaction-logs.component.scss'],
imports: [CommonModule, TranslateModule, NgSelectModule, FormsModule, ReactiveFormsModule, TableFilterPipe]
})
export class TransactionLogsComponent implements OnInit {
export class TransactionLogsComponent implements OnInit, OnDestroy {
currentPage: number = 1;
totalCount: number = 0;
renewalDataExpanded: boolean = true;
@ -39,6 +41,10 @@ export class TransactionLogsComponent implements OnInit {
maxDate: string = '';
showDateFilters: boolean = false;
// Search subject for debouncing
private searchSubject = new Subject<string>();
private destroy$ = new Subject<void>();
constructor(
private excelExportService: ExcelExportService,
private httpService: HttpURIService,
@ -48,21 +54,47 @@ export class TransactionLogsComponent implements OnInit {
// Set max date to today
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 lastMonth = new Date();
lastMonth.setDate(today.getDate() - 30);
const lastWeek = new Date();
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');
// 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();
});
}
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();
@ -121,20 +153,31 @@ export class TransactionLogsComponent implements OnInit {
});
}
// Date filter change handler
onDateFilterChange(): void {
// Validate date range
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;
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.loadTransactionLogs();
}
@ -184,6 +227,17 @@ export class TransactionLogsComponent implements OnInit {
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;
}
@ -194,14 +248,7 @@ export class TransactionLogsComponent implements OnInit {
}
onSearch(value: string): void {
this.searchText = 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);
this.searchSubject.next(value);
}
totalPages(): number {
@ -220,8 +267,6 @@ export class TransactionLogsComponent implements OnInit {
const startIndex = (this.currentPage - 1) * 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);
}
@ -233,13 +278,67 @@ export class TransactionLogsComponent implements OnInit {
}
exportDataInExcel(): void {
// Export filtered data
const dataToExport = this.allItems.length > 0 ? this.allItems : this.transactionLog;
this.excelExportService.exportExcel(dataToExport, TRANSACTION_LOGS_FILE_NAME);
// 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;
}
// 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": "عرض الجدول",
"collapse": "يطوي",
"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",
"userName": "Username",
"userNamePlaceHolder": "Enter Username",
@ -297,5 +296,21 @@
"tableCollapsed": "Table collapsed",
"showTable": "Show Table",
"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