Merge branch 'maker-checker' into dev

Brings in the UI side of maker-checker for users and roles: a Pending User
Edits tab with a compare dialog, user/role create-edit-view wired to the
pending-request flow, plus keyboard shortcuts and inline-create dialogs.

Both conflicts resolved in dev's favour:

.env.ts — auto-generated build stamp; dev's (260901) is the newer one.

share-product-general-tab.component.html — dev had deliberately removed the
per-product Dividends button, leaving a comment explaining that dividends are
declared society-wide from Share Products -> Dividends because the board
resolves one rate for the year rather than one per product. The branch simply
predates that change, so taking its side would have resurrected a button that
was removed on purpose.

checker-inbox-and-tasks.component.html and tasks-routing.module.ts auto-merged
cleanly, with both the new Pending User Edits tab and the existing Closure
Requests tab intact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-01 18:13:34 +05:30
co-authored by Claude Opus 5
55 changed files with 3152 additions and 229 deletions
@@ -123,7 +123,9 @@ export class CheckerInboxComponent implements OnInit {
];
private excludeProductEdits(items: any[]): any[] {
return (items || []).filter((item: any) => !this.productEditEntities.includes(item?.entityName));
return (items || []).filter(
(item: any) => !(this.productEditEntities.includes(item?.entityName) && item?.actionName === 'UPDATE')
);
}
/**
@@ -0,0 +1,161 @@
<!--
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/.
-->
<div class="pce-tab-content">
<div class="pce-action-bar">
<div class="pce-search-box">
<mat-icon>search</mat-icon>
<input #filterInput type="text" placeholder="Search by username" (keyup.enter)="applyFilter(filterInput.value)" />
</div>
<select class="pce-filter-select" [(ngModel)]="filterStatus" (change)="onFilterChange()">
@for (opt of statusOptions; track opt.value) {
<option [value]="opt.value">{{ opt.label }}</option>
}
</select>
<span class="pce-fb-sep"></span>
<div class="pce-presets">
@for (p of datePresets; track p.key) {
<button
type="button"
class="pce-preset-chip"
[class.pce-preset-chip--active]="activePreset === p.key"
(click)="applyPreset(p.key)"
>
{{ p.label }}
</button>
}
</div>
<button type="button" class="pce-btn pce-btn-refresh" (click)="load()" [disabled]="loading">
<fa-icon icon="sync-alt"></fa-icon>
</button>
</div>
@if (loading) {
<div class="empty-state">
<p class="empty-title">Loading&hellip;</p>
</div>
} @else if (loaded && requests.length === 0) {
<div class="empty-state">
<fa-icon icon="hourglass-half" class="empty-icon"></fa-icon>
<p class="empty-title">No {{ filterStatus | lowercase }} user requests</p>
<p class="empty-text">No pending user operations found for this filter.</p>
</div>
} @else {
<div class="table-container">
<table mat-table [dataSource]="requests" class="modern-table">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef>#</th>
<td mat-cell *matCellDef="let req">{{ req.id }}</td>
</ng-container>
<ng-container matColumnDef="requestType">
<th mat-header-cell *matHeaderCellDef>Action</th>
<td mat-cell *matCellDef="let req">
<span
class="type-badge"
[class.type-create]="req.requestType === 'CREATE_USER'"
[class.type-update]="req.requestType === 'UPDATE_USER'"
>
{{ req.requestType === 'CREATE_USER' ? 'Create User' : 'Update User' }}
</span>
</td>
</ng-container>
<ng-container matColumnDef="username">
<th mat-header-cell *matHeaderCellDef>Username</th>
<td mat-cell *matCellDef="let req" class="font-medium">{{ req.username || '—' }}</td>
</ng-container>
<ng-container matColumnDef="fullname">
<th mat-header-cell *matHeaderCellDef>Name</th>
<td mat-cell *matCellDef="let req">{{ req.firstname || '' }} {{ req.lastname || '' }}</td>
</ng-container>
<ng-container matColumnDef="email">
<th mat-header-cell *matHeaderCellDef>Email</th>
<td mat-cell *matCellDef="let req">{{ req.email || '—' }}</td>
</ng-container>
<ng-container matColumnDef="office">
<th mat-header-cell *matHeaderCellDef>Office ID</th>
<td mat-cell *matCellDef="let req">{{ req.officeId || '—' }}</td>
</ng-container>
<ng-container matColumnDef="makerUsername">
<th mat-header-cell *matHeaderCellDef>Submitted By</th>
<td mat-cell *matCellDef="let req">{{ req.makerUsername }}</td>
</ng-container>
<ng-container matColumnDef="submittedDate">
<th mat-header-cell *matHeaderCellDef>Submitted On</th>
<td mat-cell *matCellDef="let req">{{ formatDate(req.submittedDate) }}</td>
</ng-container>
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef>Status</th>
<td mat-cell *matCellDef="let req">
<span class="status-badge" [ngClass]="statusClass(req.status)">{{ req.status }}</span>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>Actions</th>
<td mat-cell *matCellDef="let req">
<div class="action-buttons">
<button
type="button"
class="btn-action btn-action--compare"
(click)="compare(req)"
title="View / Compare Details"
>
<fa-icon icon="eye"></fa-icon>
<span>View</span>
</button>
@if (req.status === 'PENDING') {
<button
type="button"
class="btn-action btn-action--approve"
(click)="approve(req)"
title="Approve User Request"
>
<fa-icon icon="check"></fa-icon>
<span>Approve</span>
</button>
<button
type="button"
class="btn-action btn-action--reject"
(click)="reject(req)"
title="Reject User Request"
>
<fa-icon icon="times"></fa-icon>
<span>Reject</span>
</button>
}
</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
<mat-paginator
[length]="totalRecords"
[pageSize]="pageSize"
[pageSizeOptions]="[10, 20, 50]"
(page)="onPageChange($event)"
></mat-paginator>
</div>
}
</div>
@@ -0,0 +1,221 @@
/**
* 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/.
*/
.pce-tab-content {
padding: 16px;
}
.pce-action-bar {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.pce-search-box {
display: flex;
align-items: center;
background: #fff;
border: 1px solid #cbd5e1;
border-radius: 6px;
padding: 4px 10px;
gap: 6px;
mat-icon {
font-size: 18px;
color: #64748b;
}
input {
border: none;
outline: none;
font-size: 0.88rem;
}
}
.pce-filter-select {
padding: 6px 12px;
border-radius: 6px;
border: 1px solid #cbd5e1;
background: #fff;
font-size: 0.88rem;
}
.pce-fb-sep {
height: 24px;
width: 1px;
background: #e2e8f0;
}
.pce-presets {
display: flex;
gap: 6px;
}
.pce-preset-chip {
padding: 4px 10px;
border-radius: 16px;
border: 1px solid #cbd5e1;
background: #f8fafc;
font-size: 0.8rem;
cursor: pointer;
transition: all 0.2s ease;
&--active {
background: #2563eb;
color: #fff;
border-color: #2563eb;
}
}
.pce-btn-refresh {
padding: 6px 12px;
border-radius: 6px;
border: 1px solid #cbd5e1;
background: #fff;
cursor: pointer;
}
.table-container {
background: #fff;
border-radius: 8px;
border: 1px solid #e2e8f0;
overflow: hidden;
}
.modern-table {
width: 100%;
th {
background: #f8fafc;
color: #475569;
font-weight: 600;
font-size: 0.82rem;
padding: 12px 14px;
border-bottom: 2px solid #e2e8f0;
}
td {
padding: 10px 14px;
font-size: 0.85rem;
border-bottom: 1px solid #f1f5f9;
}
}
.type-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.72rem;
font-weight: 600;
&.type-create {
background-color: #e0f2fe;
color: #0369a1;
}
&.type-update {
background-color: #f3e8ff;
color: #7e22ce;
}
}
.status-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
&--pending {
background-color: #fef3c7;
color: #b45309;
}
&--approved {
background-color: #d1fae5;
color: #065f46;
}
&--rejected {
background-color: #fee2e2;
color: #991b1b;
}
}
.action-buttons {
display: flex;
align-items: center;
gap: 6px;
}
.btn-action {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
border-radius: 4px;
border: 1px solid transparent;
font-size: 0.75rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.15s;
&--compare {
background: #f1f5f9;
color: #334155;
border-color: #cbd5e1;
&:hover {
background: #e2e8f0;
}
}
&--approve {
background: #ecfdf5;
color: #059669;
border-color: #a7f3d0;
&:hover {
background: #d1fae5;
}
}
&--reject {
background: #fef2f2;
color: #dc2626;
border-color: #fecaca;
&:hover {
background: #fee2e2;
}
}
}
.empty-state {
text-align: center;
padding: 48px 16px;
color: #64748b;
.empty-icon {
font-size: 32px;
margin-bottom: 8px;
}
.empty-title {
font-size: 1.1rem;
font-weight: 600;
margin: 0;
}
.empty-text {
font-size: 0.88rem;
margin-top: 4px;
}
}
@@ -0,0 +1,323 @@
/**
* 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',
roleNames: '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();
});
}
});
}
}
@@ -0,0 +1,75 @@
<!--
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/.
-->
<h2 mat-dialog-title class="dialog-title">
<span class="dialog-title__action"
>{{ data.requestType === 'CREATE_USER' ? 'Create User' : 'Update User' }} Request</span
>
<span class="dialog-title__user">&mdash; {{ data.username }}</span>
</h2>
<mat-dialog-content class="dialog-body">
<div class="meta-banner">
<div class="meta-item">
<span class="meta-label">Request ID</span>
<span class="meta-val">#{{ data.requestId }}</span>
</div>
<div class="meta-item">
<span class="meta-label">Submitted By</span>
<span class="meta-val">{{ data.makerUsername }}</span>
</div>
<div class="meta-item">
<span class="meta-label">Submitted On</span>
<span class="meta-val">{{ data.submittedDate }}</span>
</div>
<div class="meta-item">
<span class="meta-label">Status</span>
<span
class="status-badge"
[class.badge-pending]="data.status === 'PENDING'"
[class.badge-approved]="data.status === 'APPROVED'"
[class.badge-rejected]="data.status === 'REJECTED'"
>
{{ data.status }}
</span>
</div>
</div>
@if (data.rejectReason) {
<div class="reject-banner"><strong>Rejection Reason:</strong> {{ data.rejectReason }}</div>
}
<table class="compare-table">
<thead>
<tr>
<th>Field</th>
@if (data.requestType === 'UPDATE_USER') {
<th>Current Value</th>
}
<th>Proposed Value</th>
</tr>
</thead>
<tbody>
@for (r of data.rows; track r.key) {
<tr>
<td class="col-field">{{ r.label }}</td>
@if (data.requestType === 'UPDATE_USER') {
<td class="col-curr">{{ r.current != null && r.current !== '' ? r.current : '—' }}</td>
}
<td class="col-prop" [class.highlight-change]="data.requestType === 'UPDATE_USER' && r.current !== r.value">
{{ r.value != null && r.value !== '' ? r.value : '—' }}
</td>
</tr>
}
</tbody>
</table>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button mat-dialog-close class="btn-close">Close</button>
</mat-dialog-actions>
@@ -0,0 +1,141 @@
/**
* 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/.
*/
.dialog-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 1.15rem;
font-weight: 600;
color: #1e293b;
margin: 0;
padding: 16px 20px;
border-bottom: 1px solid #e2e8f0;
&__action {
color: #2563eb;
}
&__user {
color: #475569;
}
}
.dialog-body {
padding: 16px 20px;
max-height: 70vh;
}
.meta-banner {
display: flex;
flex-wrap: wrap;
gap: 16px;
padding: 12px 16px;
background-color: #f8fafc;
border-radius: 8px;
border: 1px solid #e2e8f0;
margin-bottom: 16px;
}
.meta-item {
display: flex;
flex-direction: column;
gap: 2px;
}
.meta-label {
font-size: 0.72rem;
text-transform: uppercase;
color: #64748b;
font-weight: 600;
letter-spacing: 0.04em;
}
.meta-val {
font-size: 0.88rem;
font-weight: 500;
color: #1e293b;
}
.status-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
&.badge-pending {
background-color: #fef3c7;
color: #b45309;
}
&.badge-approved {
background-color: #d1fae5;
color: #065f46;
}
&.badge-rejected {
background-color: #fee2e2;
color: #991b1b;
}
}
.reject-banner {
background-color: #fff1f2;
border: 1px solid #fecdd3;
color: #9f1239;
padding: 10px 14px;
border-radius: 6px;
font-size: 0.85rem;
margin-bottom: 16px;
}
.compare-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
th {
text-align: left;
padding: 8px 12px;
background: #f1f5f9;
color: #475569;
font-weight: 600;
border-bottom: 2px solid #cbd5e1;
}
td {
padding: 8px 12px;
border-bottom: 1px solid #e2e8f0;
}
.col-field {
font-weight: 500;
color: #334155;
width: 28%;
}
.col-curr {
color: #64748b;
}
.col-prop {
color: #0f172a;
font-weight: 500;
&.highlight-change {
background-color: #f0fdf4;
color: #15803d;
font-weight: 600;
}
}
}
.btn-close {
color: #64748b;
}
@@ -0,0 +1,57 @@
/**
* 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, inject } from '@angular/core';
import {
MatDialogRef,
MAT_DIALOG_DATA,
MatDialogTitle,
MatDialogContent,
MatDialogActions,
MatDialogClose
} from '@angular/material/dialog';
import { CdkScrollable } from '@angular/cdk/scrolling';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
export interface UserEditCompareRow {
key: string;
label: string;
current: any;
value: any;
}
export interface UserEditCompareDialogData {
requestId: string | number;
requestType: string;
username: string;
rows: UserEditCompareRow[];
status?: string;
makerUsername?: string;
submittedDate?: string | null;
checkerUsername?: string;
checkedDate?: string | null;
rejectReason?: string;
}
@Component({
selector: 'mifosx-user-edit-compare-dialog',
templateUrl: './user-edit-compare-dialog.component.html',
styleUrls: ['./user-edit-compare-dialog.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatDialogTitle,
CdkScrollable,
MatDialogContent,
MatDialogActions,
MatDialogClose
]
})
export class UserEditCompareDialogComponent {
dialogRef = inject<MatDialogRef<UserEditCompareDialogComponent>>(MatDialogRef);
data = inject<UserEditCompareDialogData>(MAT_DIALOG_DATA);
}
@@ -119,6 +119,15 @@
<fa-icon icon="edit" class="ac-tab__ic"></fa-icon>
<span>Pending Client Edits</span>
</a>
<a
class="ac-tab"
routerLink="./pending-user-edits"
routerLinkActive="ac-tab--active"
*mifosxHasPermission="'ALL_FUNCTIONS_READ'"
>
<fa-icon icon="user-cog" class="ac-tab__ic"></fa-icon>
<span>Pending User Approvals</span>
</a>
<a
class="ac-tab"
routerLink="./rejected-account"
+6
View File
@@ -36,6 +36,7 @@ import { PendingSavingsAccountEditsComponent } from './checker-inbox-and-tasks-t
import { PendingShareAccountEditsComponent } from './checker-inbox-and-tasks-tabs/pending-share-account-edits/pending-share-account-edits.component';
import { PendingShareAccountsComponent } from './checker-inbox-and-tasks-tabs/pending-share-accounts/pending-share-accounts.component';
import { PendingClientEditsComponent } from './checker-inbox-and-tasks-tabs/pending-client-edits/pending-client-edits.component';
import { PendingUserEditsComponent } from './checker-inbox-and-tasks-tabs/pending-user-edits/pending-user-edits.component';
import { ViewCheckerInboxComponent } from './view-checker-inbox/view-checker-inbox.component';
/** Custom Resolvers */
@@ -168,6 +169,11 @@ const routes: Routes = [
component: PendingClientEditsComponent,
data: { title: 'Pending Client Edits' }
},
{
path: 'pending-user-edits',
component: PendingUserEditsComponent,
data: { title: 'Pending User Approvals' }
},
{
path: 'withdrawal-requests',
component: WithdrawalRequestsComponent,
@@ -65,7 +65,10 @@ export class ViewCheckerInboxComponent {
];
get isProductEditEntry(): boolean {
return this.productEditEntities.includes(this.checkerInboxDetail?.entityName);
return (
this.productEditEntities.includes(this.checkerInboxDetail?.entityName) &&
this.checkerInboxDetail?.actionName === 'UPDATE'
);
}
/**