/** * 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/. */ import { Component, OnInit, inject } from '@angular/core'; import { FormsModule } from '@angular/forms'; 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'; import { MatIcon } from '@angular/material/icon'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { UsersService } from 'app/users/users.service'; import { SettingsService } from 'app/settings/settings.service'; import { ConfirmationDialogComponent } from 'app/shared/confirmation-dialog/confirmation-dialog.component'; import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component'; import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base'; import { InputBase } from 'app/shared/form-dialog/formfield/model/input-base'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { UserEditCompareDialogComponent, UserEditCompareRow, UserEditCompareDialogData } from './user-edit-compare-dialog/user-edit-compare-dialog.component'; const USER_FIELD_LABELS: Record = { username: 'Username', firstname: 'First Name', lastname: 'Last Name', email: 'Email Address', mobileNo: 'Mobile Number', officeId: 'Office', officeName: 'Office Name', staffId: 'Staff Member', roles: 'Assigned Roles', otpDeliveryMethod: '2FA / OTP Channel', sendPasswordToEmail: 'Send Password To Email', passwordNeverExpires: 'Password Never Expires', password: 'Password' }; @Component({ selector: 'mifosx-pending-user-edits', templateUrl: './pending-user-edits.component.html', styleUrls: ['./pending-user-edits.component.scss'], imports: [ ...STANDALONE_SHARED_IMPORTS, FormsModule, FaIconComponent, MatTable, MatColumnDef, MatHeaderCellDef, MatHeaderCell, MatCellDef, MatCell, MatHeaderRowDef, MatHeaderRow, MatRowDef, MatRow, MatIcon, MatPaginatorModule ] }) export class PendingUserEditsComponent implements OnInit { private usersService = inject(UsersService); private settingsService = inject(SettingsService); private dialog = inject(MatDialog); requests: any[] = []; loading = false; loaded = false; filterStatus = 'PENDING'; filterUsername = ''; fromDate: Date | null = null; toDate: Date | null = null; maxDate = new Date(); activePreset = 'all'; pageIndex = 0; pageSize = 20; totalRecords = 0; displayedColumns = [ 'id', 'requestType', 'username', 'fullname', 'email', 'office', 'makerUsername', 'submittedDate', 'status', 'actions' ]; statusOptions = [ { value: 'PENDING', label: 'Pending Approval' }, { value: 'APPROVED', label: 'Approved' }, { value: 'REJECTED', label: 'Rejected' }, { value: '', label: 'All Statuses' } ]; datePresets = [ { key: 'today', label: 'Today' }, { key: '7d', label: 'Last 7 Days' }, { key: '30d', label: 'Last 30 Days' }, { key: 'all', label: 'All Dates' } ]; ngOnInit(): void { this.load(); } load(): void { this.loading = true; const fromStr = this.fromDate ? this.formatIsoDate(this.fromDate) : undefined; const toStr = this.toDate ? this.formatIsoDate(this.toDate) : undefined; const offset = this.pageIndex * this.pageSize; this.usersService .getPendingUsers(this.filterStatus, this.filterUsername, fromStr, toStr, offset, this.pageSize) .subscribe({ next: (response: any) => { this.requests = response.pageItems || []; this.totalRecords = response.totalFilteredRecords || this.requests.length; this.loading = false; this.loaded = true; }, error: () => { this.requests = []; this.loading = false; this.loaded = true; } }); } onFilterChange(): void { this.pageIndex = 0; this.load(); } applyFilter(query: string): void { this.filterUsername = query; this.pageIndex = 0; this.load(); } applyPreset(key: string): void { this.activePreset = key; const now = new Date(); if (key === 'today') { this.fromDate = new Date(now.getFullYear(), now.getMonth(), now.getDate()); this.toDate = new Date(now.getFullYear(), now.getMonth(), now.getDate()); } else if (key === '7d') { const past = new Date(); past.setDate(past.getDate() - 7); this.fromDate = past; this.toDate = now; } else if (key === '30d') { const past = new Date(); past.setDate(past.getDate() - 30); this.fromDate = past; this.toDate = now; } else { this.fromDate = null; this.toDate = null; } this.pageIndex = 0; this.load(); } onPageChange(event: PageEvent): void { this.pageIndex = event.pageIndex; this.pageSize = event.pageSize; this.load(); } formatDate(val: any): string { if (!val) { return '—'; } const d = new Date(val); return isNaN(d.getTime()) ? String(val) : d.toLocaleString(); } private formatIsoDate(d: Date): string { const yyyy = d.getFullYear(); const mm = String(d.getMonth() + 1).padStart(2, '0'); const dd = String(d.getDate()).padStart(2, '0'); return `${yyyy}-${mm}-${dd}`; } statusClass(status: string): string { if (status === 'PENDING') { return 'status-badge--pending'; } if (status === 'APPROVED') { return 'status-badge--approved'; } if (status === 'REJECTED') { return 'status-badge--rejected'; } return ''; } compare(req: any): void { this.usersService.getPendingUser(req.id).subscribe((detail: any) => { const rows: UserEditCompareRow[] = []; const proposed = detail.proposedValues || {}; const original = detail.originalValues || {}; const allKeys = Array.from( new Set([ ...Object.keys(original), ...Object.keys(proposed) ]) ); for (const k of allKeys) { if (k === 'repeatPassword' || k === 'pendingLockKey' || k === 'id') { continue; } const label = USER_FIELD_LABELS[k] || k; let curVal = original[k]; let propVal = proposed[k]; if (Array.isArray(curVal)) { curVal = curVal.join(', '); } if (Array.isArray(propVal)) { propVal = propVal.join(', '); } rows.push({ key: k, label, current: curVal, value: propVal }); } const data: UserEditCompareDialogData = { requestId: detail.id, requestType: detail.requestType, username: detail.username || req.username, rows, status: detail.status, makerUsername: detail.makerUsername, submittedDate: this.formatDate(detail.submittedDate), checkerUsername: detail.checkerUsername, checkedDate: this.formatDate(detail.checkedDate), rejectReason: detail.rejectReason }; this.dialog.open(UserEditCompareDialogComponent, { data, width: '650px' }); }); } approve(req: any): void { const actionLabel = req.requestType === 'CREATE_USER' ? 'creation' : 'update'; const dialogRef = this.dialog.open(ConfirmationDialogComponent, { data: { heading: `Approve User ${actionLabel}`, dialogContext: `Are you sure you want to approve this user ${actionLabel} for '${req.username}'?` } }); dialogRef.afterClosed().subscribe((confirmed: boolean) => { if (confirmed) { this.usersService.approvePendingUser(req.id).subscribe(() => { this.load(); }); } }); } reject(req: any): void { const formfields: FormfieldBase[] = [ new InputBase({ controlType: 'input', key: 'reason', label: 'Rejection Reason', required: true, order: 1 }) ]; const dialogRef = this.dialog.open(FormDialogComponent, { data: { title: `Reject User Request`, layout: { addButtonText: 'Reject' }, formfields } }); dialogRef.afterClosed().subscribe((result: any) => { if (result && result.data && result.data.reason) { this.usersService.rejectPendingUser(req.id, result.data.reason).subscribe(() => { this.load(); }); } }); } }