323 lines
8.7 KiB
TypeScript
323 lines
8.7 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/.
|
|
*/
|
|
|
|
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<string, string> = {
|
|
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();
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|