/** * 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(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(); } }