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.
275 lines
8.2 KiB
TypeScript
275 lines
8.2 KiB
TypeScript
/**
|
|
* Copyright since 2025 Mifos Initiative
|
|
*
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
*/
|
|
|
|
/** Angular Imports */
|
|
import { Component, OnInit, inject } from '@angular/core';
|
|
import { SelectionModel } from '@angular/cdk/collections';
|
|
import * as _ from 'lodash';
|
|
import {
|
|
MatTable,
|
|
MatColumnDef,
|
|
MatHeaderCellDef,
|
|
MatHeaderCell,
|
|
MatCellDef,
|
|
MatCell,
|
|
MatHeaderRowDef,
|
|
MatHeaderRow,
|
|
MatRowDef,
|
|
MatRow
|
|
} from '@angular/material/table';
|
|
import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
|
|
import { MatDialog } from '@angular/material/dialog';
|
|
|
|
/** Dialog Imports */
|
|
import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component';
|
|
import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base';
|
|
import { DatepickerBase } from 'app/shared/form-dialog/formfield/model/datepicker-base';
|
|
|
|
/** Custom Services */
|
|
import { TasksService } from '../../tasks.service';
|
|
import { SettingsService } from 'app/settings/settings.service';
|
|
import { Dates } from 'app/core/utils/dates';
|
|
import { KeyValuePipe } from '@angular/common';
|
|
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
|
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',
|
|
styleUrls: ['./client-approval.component.scss'],
|
|
imports: [
|
|
...STANDALONE_SHARED_IMPORTS,
|
|
FaIconComponent,
|
|
MatTable,
|
|
MatColumnDef,
|
|
MatHeaderCellDef,
|
|
MatHeaderCell,
|
|
MatCheckbox,
|
|
MatCellDef,
|
|
MatCell,
|
|
MatHeaderRowDef,
|
|
MatHeaderRow,
|
|
MatRowDef,
|
|
MatRow,
|
|
KeyValuePipe,
|
|
AccountsFilterPipe,
|
|
MatPaginatorModule
|
|
]
|
|
})
|
|
export class ClientApprovalComponent implements OnInit {
|
|
private dialog = inject(MatDialog);
|
|
private dateUtils = inject(Dates);
|
|
private settingsService = inject(SettingsService);
|
|
private tasksService = inject(TasksService);
|
|
|
|
/** 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[];
|
|
/** Displayed Columns */
|
|
displayedColumns: string[] = [
|
|
'select',
|
|
'name',
|
|
'clientId',
|
|
'staff'
|
|
];
|
|
|
|
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. */
|
|
isAllSelected(dataSource2: any) {
|
|
if (dataSource2) {
|
|
const numSelected = this.selection.selected;
|
|
return _.difference(dataSource2, numSelected).length === 0;
|
|
}
|
|
}
|
|
|
|
/** Selects all rows if they are not all selected; otherwise clear selection. */
|
|
masterToggle(dataSource3: any) {
|
|
if (dataSource3) {
|
|
this.isAllSelected(dataSource3)
|
|
? dataSource3.forEach((row: any) => this.selection.deselect(row))
|
|
: dataSource3.forEach((row: any) => this.selection.select(row));
|
|
}
|
|
}
|
|
|
|
/** The label for the checkbox on the passed row */
|
|
checkboxLabel(row?: any): string {
|
|
if (!row) {
|
|
return `${this.isAllSelected(row) ? 'select' : 'deselect'} all`;
|
|
}
|
|
return `${this.selection.isSelected(row) ? 'deselect' : 'select'} row ${row.position + 1}`;
|
|
}
|
|
|
|
approveClients() {
|
|
const formfields: FormfieldBase[] = [
|
|
new DatepickerBase({
|
|
controlName: 'actDate',
|
|
label: 'Date',
|
|
value: new Date(),
|
|
type: 'datetime-local',
|
|
required: true
|
|
})
|
|
];
|
|
const data = {
|
|
title: 'Enter Clients Activation Date',
|
|
layout: { addButtonText: 'Confirm' },
|
|
formfields: formfields
|
|
};
|
|
const clientApprovalDialogRef = this.dialog.open(FormDialogComponent, { data });
|
|
clientApprovalDialogRef.afterClosed().subscribe((response: any) => {
|
|
if (response.data) {
|
|
this.bulkClientApproval(response.data);
|
|
}
|
|
});
|
|
}
|
|
|
|
bulkClientApproval(submittedData: any) {
|
|
const dateFormat = this.settingsService.dateFormat;
|
|
const activationDate = this.dateUtils.formatDate(submittedData.value.actDate, dateFormat);
|
|
const locale = this.settingsService.language.code;
|
|
const formData = {
|
|
dateFormat,
|
|
activationDate,
|
|
locale
|
|
};
|
|
const selectedAccounts = this.selection.selected.length;
|
|
const listSelectedAccounts = this.selection.selected;
|
|
let activatedAccounts = 0;
|
|
this.batchRequests = [];
|
|
let reqId = 1;
|
|
listSelectedAccounts.forEach((element: any) => {
|
|
const url = 'clients/' + element.id + '?command=activate';
|
|
const bodyData = JSON.stringify(formData);
|
|
const batchData = { requestId: reqId++, relativeUrl: url, method: 'POST', body: bodyData };
|
|
this.batchRequests.push(batchData);
|
|
});
|
|
this.tasksService.submitBatchData(this.batchRequests).subscribe((response: any) => {
|
|
response.forEach((responseEle: any) => {
|
|
if (responseEle.statusCode === '200') {
|
|
activatedAccounts++;
|
|
responseEle.body = JSON.parse(responseEle.body);
|
|
if (selectedAccounts === activatedAccounts) {
|
|
this.selection.clear();
|
|
this.reload();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Refetches the current page after a bulk approval.
|
|
*/
|
|
reload() {
|
|
this.load();
|
|
}
|
|
}
|