feat(tasks): backend-driven pagination, search and date filters for Client Approval
Client Approval previously fetched up to 1000 pending clients via a route resolver and did name-search/sorting client-side (the search box was actually dead — it set a MatTableDataSource filter that the grouped-table view never read). Reworked to mirror PendingClientEditsComponent: real mat-paginator against /clients (offset/limit), server-side displayName search, orderBy=submittedOnDate for latest-first, and All/Today/Yesterday presets backed by the new fromDate/toDate params. Dropped the now-unused GetGroupedClientsData resolver. Also: Account Number column -> Client ID (element.id), disabled the Approve button with a "N selected" indicator when nothing's checked, and a redesigned action bar / empty-state (context-aware message for zero-results-from-search vs. zero-results-from-date-filter vs. truly nothing pending) — plus the plain-div search box and matched heights across chips/search/button, replacing an earlier mat-form-field version that Material's own outlined-field box model kept misaligning against the pill buttons.
This commit is contained in:
+109
-42
@@ -8,11 +8,9 @@
|
||||
|
||||
/** Angular Imports */
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
import { SelectionModel } from '@angular/cdk/collections';
|
||||
import * as _ from 'lodash';
|
||||
import {
|
||||
MatTableDataSource,
|
||||
MatTable,
|
||||
MatColumnDef,
|
||||
MatHeaderCellDef,
|
||||
@@ -24,6 +22,7 @@ import {
|
||||
MatRowDef,
|
||||
MatRow
|
||||
} from '@angular/material/table';
|
||||
import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
|
||||
/** Dialog Imports */
|
||||
@@ -41,6 +40,12 @@ import { MatCheckbox } from '@angular/material/checkbox';
|
||||
import { AccountsFilterPipe } from '../../../pipes/accounts-filter.pipe';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
/**
|
||||
* Checker-facing list of Clients pending approval.
|
||||
* Pagination, name search, and submitted-date filtering all happen server-side via the
|
||||
* standard /clients list API (offset/limit, displayName, fromDate/toDate) — mirrors
|
||||
* PendingClientEditsComponent's load()/onPageChange()/applyPreset() pattern.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-client-approval',
|
||||
templateUrl: './client-approval.component.html',
|
||||
@@ -60,53 +65,122 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
MatRowDef,
|
||||
MatRow,
|
||||
KeyValuePipe,
|
||||
AccountsFilterPipe
|
||||
AccountsFilterPipe,
|
||||
MatPaginatorModule
|
||||
]
|
||||
})
|
||||
export class ClientApprovalComponent {
|
||||
private route = inject(ActivatedRoute);
|
||||
export class ClientApprovalComponent implements OnInit {
|
||||
private dialog = inject(MatDialog);
|
||||
private dateUtils = inject(Dates);
|
||||
private router = inject(Router);
|
||||
private settingsService = inject(SettingsService);
|
||||
private tasksService = inject(TasksService);
|
||||
|
||||
/** Grouped Clients Data */
|
||||
groupedClients: any;
|
||||
/** Checks to show the data */
|
||||
/** Current page of pending clients (from the server), grouped by office for display */
|
||||
groupedClients: any = {};
|
||||
/** True once the first page has loaded and it contains at least one client */
|
||||
showData = false;
|
||||
loading = false;
|
||||
loaded = false;
|
||||
/** Row Selection Data — selection lives across the whole session, not just the current page */
|
||||
selection = new SelectionModel<any>(true, []);
|
||||
/** Batch Requests */
|
||||
batchRequests: any[];
|
||||
/** Datasource */
|
||||
dataSource: MatTableDataSource<any>;
|
||||
/** Row Selection Data */
|
||||
selection: SelectionModel<any>;
|
||||
/** Displayed Columns */
|
||||
displayedColumns: string[] = [
|
||||
'select',
|
||||
'name',
|
||||
'accountNumber',
|
||||
'clientId',
|
||||
'staff'
|
||||
];
|
||||
|
||||
/**
|
||||
* Retrieves the grouped client data from `resolve`.
|
||||
* @param {ActivatedRoute} route Activated Route.
|
||||
* @param {Dialog} dialog MatDialog.
|
||||
* @param {Dates} dateUtils Date Utils.
|
||||
* @param {router} router Router.
|
||||
* @param {SettingsService} settingsService Settings Service.
|
||||
* @param {TasksService} tasksService Tasks Service.
|
||||
*/
|
||||
constructor() {
|
||||
this.route.data.subscribe((data: { groupedClientData: any }) => {
|
||||
this.groupedClients = _.groupBy(data.groupedClientData.pageItems, 'officeName');
|
||||
if (Object.keys(this.groupedClients).length) {
|
||||
this.showData = true;
|
||||
}
|
||||
this.dataSource = new MatTableDataSource(data.groupedClientData.pageItems);
|
||||
this.selection = new SelectionModel(true, []);
|
||||
});
|
||||
totalRecords = 0;
|
||||
pageSize = 20;
|
||||
pageIndex = 0;
|
||||
pageSizeOptions = [
|
||||
10,
|
||||
20,
|
||||
50
|
||||
];
|
||||
|
||||
searchTerm = '';
|
||||
activePreset = 'all';
|
||||
readonly datePresets = [
|
||||
{ key: 'all', label: 'All requests' },
|
||||
{ key: 'today', label: 'Today' },
|
||||
{ key: 'yesterday', label: 'Yesterday' }
|
||||
];
|
||||
private fromDate: string | undefined;
|
||||
private toDate: string | undefined;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.load();
|
||||
}
|
||||
|
||||
load(): void {
|
||||
this.loading = true;
|
||||
this.tasksService
|
||||
.getGroupedClientsData(
|
||||
this.searchTerm.trim() || undefined,
|
||||
this.fromDate,
|
||||
this.toDate,
|
||||
this.pageIndex * this.pageSize,
|
||||
this.pageSize
|
||||
)
|
||||
.subscribe({
|
||||
next: (res: any) => {
|
||||
const clients = res?.pageItems ?? [];
|
||||
this.groupedClients = _.groupBy(clients, 'officeName');
|
||||
this.totalRecords = Number(res?.totalFilteredRecords ?? 0);
|
||||
this.showData = this.totalRecords > 0 || Object.keys(this.groupedClients).length > 0;
|
||||
this.loading = false;
|
||||
this.loaded = true;
|
||||
},
|
||||
error: () => {
|
||||
this.groupedClients = {};
|
||||
this.totalRecords = 0;
|
||||
this.loading = false;
|
||||
this.loaded = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onPageChange(event: PageEvent): void {
|
||||
this.pageSize = event.pageSize;
|
||||
this.pageIndex = event.pageIndex;
|
||||
this.load();
|
||||
}
|
||||
|
||||
applyFilter(filterValue: string = ''): void {
|
||||
this.searchTerm = filterValue;
|
||||
this.pageIndex = 0;
|
||||
this.load();
|
||||
}
|
||||
|
||||
applyPreset(key: string): void {
|
||||
this.activePreset = key;
|
||||
const today = new Date(this.settingsService.businessDate);
|
||||
today.setHours(0, 0, 0, 0);
|
||||
if (key === 'today') {
|
||||
this.fromDate = this.isoDate(today);
|
||||
this.toDate = this.isoDate(today);
|
||||
} else if (key === 'yesterday') {
|
||||
const yesterday = new Date(today);
|
||||
yesterday.setDate(today.getDate() - 1);
|
||||
this.fromDate = this.isoDate(yesterday);
|
||||
this.toDate = this.isoDate(yesterday);
|
||||
} else {
|
||||
this.fromDate = undefined;
|
||||
this.toDate = undefined;
|
||||
}
|
||||
this.pageIndex = 0;
|
||||
this.load();
|
||||
}
|
||||
|
||||
private isoDate(d: Date): string {
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${day}`;
|
||||
}
|
||||
|
||||
/** Whether the number of selected elements matches the total number of rows. */
|
||||
@@ -183,6 +257,7 @@ export class ClientApprovalComponent {
|
||||
activatedAccounts++;
|
||||
responseEle.body = JSON.parse(responseEle.body);
|
||||
if (selectedAccounts === activatedAccounts) {
|
||||
this.selection.clear();
|
||||
this.reload();
|
||||
}
|
||||
}
|
||||
@@ -190,18 +265,10 @@ export class ClientApprovalComponent {
|
||||
});
|
||||
}
|
||||
|
||||
applyFilter(filterValue: string = '') {
|
||||
this.dataSource.filter = filterValue.trim().toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Refetches data for the component
|
||||
* TODO: Replace by a custom reload component instead of hard-coded back-routing.
|
||||
* Refetches the current page after a bulk approval.
|
||||
*/
|
||||
reload() {
|
||||
const url: string = this.router.url;
|
||||
this.router
|
||||
.navigateByUrl(`/checker-inbox-and-tasks`, { skipLocationChange: true })
|
||||
.then(() => this.router.navigate([url]));
|
||||
this.load();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user