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:
2026-07-30 10:31:55 +05:30
parent 4ea833ff49
commit 1efb520009
6 changed files with 417 additions and 158 deletions
@@ -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();
}
}