feat(tasks,savings,cashier): facility charge mapping screen, chained approve+activate, popup diff dialog, cash/transfer-only teller flow
Adds a Products > Facility Charge Mappings screen so each of the 16 facility toggles can be pointed at a real charge, backing the mapped-charge validation added on the backend. Bulk-approve on the pending accounts screen now chains straight into activation (matching the single-account approve flow) instead of leaving accounts sitting in Approved; the same screen's Confirm button no longer stays disabled on pre-filled but untouched dialogs. Add Charge is hidden on not-yet-active savings accounts, and Transfer Funds is disabled entirely pending a proper transfer flow. The pending savings-account edit "Proposed Changes" panel is now a MatDialog showing only the fields that actually changed, replacing the old always-open inline diff table. The teller transaction-entry Payment Type dropdown is filtered to Cash/Transfer only, mirroring the backend's retirement of the other system-defined payment types so share withdrawal and GL transfers now settle the same way real transfers do. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+60
-24
@@ -9,6 +9,7 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { SelectionModel } from '@angular/cdk/collections';
|
||||
import { switchMap } from 'rxjs/operators';
|
||||
import {
|
||||
MatTableDataSource,
|
||||
MatTable,
|
||||
@@ -141,7 +142,8 @@ export class PendingAccountComponent {
|
||||
data: {
|
||||
title: 'Enter Approval Date',
|
||||
layout: { addButtonText: 'Confirm' },
|
||||
formfields
|
||||
formfields,
|
||||
pristine: false
|
||||
}
|
||||
});
|
||||
dialogRef.afterClosed().subscribe((response: any) => {
|
||||
@@ -166,7 +168,8 @@ export class PendingAccountComponent {
|
||||
data: {
|
||||
title: 'Enter Rejection Date',
|
||||
layout: { addButtonText: 'Confirm' },
|
||||
formfields
|
||||
formfields,
|
||||
pristine: false
|
||||
}
|
||||
});
|
||||
dialogRef.afterClosed().subscribe((response: any) => {
|
||||
@@ -183,7 +186,7 @@ export class PendingAccountComponent {
|
||||
const payload = { rejectedOnDate, dateFormat, locale };
|
||||
|
||||
const selected = this.selection.selected;
|
||||
let done = 0;
|
||||
let settled = 0;
|
||||
selected.forEach((account: any) => {
|
||||
const depositTypeId = account.depositType?.id;
|
||||
let reject$;
|
||||
@@ -196,39 +199,72 @@ export class PendingAccountComponent {
|
||||
} else {
|
||||
reject$ = this.tasksService.rejectSavingsAccount(account.id, payload);
|
||||
}
|
||||
reject$.subscribe(() => {
|
||||
done++;
|
||||
if (done === selected.length) {
|
||||
this.reload();
|
||||
reject$.subscribe({
|
||||
next: () => {
|
||||
settled++;
|
||||
if (settled === selected.length) {
|
||||
this.reload();
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
settled++;
|
||||
if (settled === selected.length) {
|
||||
this.reload();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Approves an account, then immediately activates it on the same date -- Approve and Activate
|
||||
* are still two separate Fineract commands underneath (each shows up in the audit trail), but
|
||||
* the checker only sees and clicks through the one bulk action, matching what happens when
|
||||
* approving from inside an individual account (see e.g. ApproveSavingsAccountComponent).
|
||||
*/
|
||||
private approveAndActivate$(account: any, approvedOnDate: string, dateFormat: string, locale: string) {
|
||||
const approvePayload = { approvedOnDate, dateFormat, locale };
|
||||
const activatePayload = { activatedOnDate: approvedOnDate, dateFormat, locale };
|
||||
const depositTypeId = account.depositType?.id;
|
||||
|
||||
let approve$;
|
||||
let activate$;
|
||||
if (account.accountType === 'Current') {
|
||||
approve$ = this.tasksService.approveCurrentAccount(account.id, approvePayload);
|
||||
activate$ = () => this.tasksService.activateCurrentAccount(account.id, activatePayload);
|
||||
} else if (depositTypeId === 200) {
|
||||
approve$ = this.tasksService.approveFDAccount(account.id, approvePayload);
|
||||
activate$ = () => this.tasksService.activateFDAccount(account.id, activatePayload);
|
||||
} else if (depositTypeId === 300) {
|
||||
approve$ = this.tasksService.approveRDAccount(account.id, approvePayload);
|
||||
activate$ = () => this.tasksService.activateRDAccount(account.id, activatePayload);
|
||||
} else {
|
||||
approve$ = this.tasksService.approveSavingsAccount(account.id, approvePayload);
|
||||
activate$ = () => this.tasksService.activateSavingsAccount(account.id, activatePayload);
|
||||
}
|
||||
return approve$.pipe(switchMap(activate$));
|
||||
}
|
||||
|
||||
private bulkApprove(submittedData: any) {
|
||||
const dateFormat = this.settingsService.dateFormat;
|
||||
const locale = this.settingsService.language.code;
|
||||
const approvedOnDate = this.dateUtils.formatDate(submittedData.value.approvedOnDate, dateFormat);
|
||||
const payload = { approvedOnDate, dateFormat, locale };
|
||||
|
||||
const selected = this.selection.selected;
|
||||
let done = 0;
|
||||
let settled = 0;
|
||||
selected.forEach((account: any) => {
|
||||
const depositTypeId = account.depositType?.id;
|
||||
let approve$;
|
||||
if (account.accountType === 'Current') {
|
||||
approve$ = this.tasksService.approveCurrentAccount(account.id, payload);
|
||||
} else if (depositTypeId === 200) {
|
||||
approve$ = this.tasksService.approveFDAccount(account.id, payload);
|
||||
} else if (depositTypeId === 300) {
|
||||
approve$ = this.tasksService.approveRDAccount(account.id, payload);
|
||||
} else {
|
||||
approve$ = this.tasksService.approveSavingsAccount(account.id, payload);
|
||||
}
|
||||
approve$.subscribe(() => {
|
||||
done++;
|
||||
if (done === selected.length) {
|
||||
this.reload();
|
||||
this.approveAndActivate$(account, approvedOnDate, dateFormat, locale).subscribe({
|
||||
next: () => {
|
||||
settled++;
|
||||
if (settled === selected.length) {
|
||||
this.reload();
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
settled++;
|
||||
if (settled === selected.length) {
|
||||
this.reload();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+7
-63
@@ -68,8 +68,13 @@
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
||||
<td mat-cell *matCellDef="let edit">
|
||||
<button mat-stroked-button class="psae-btn psae-btn-view" (click)="viewCompare(edit)">
|
||||
<fa-icon icon="eye" class="m-r-10"></fa-icon>Compare
|
||||
<button
|
||||
mat-stroked-button
|
||||
class="psae-btn psae-btn-view"
|
||||
[disabled]="loadingCompareFor === edit"
|
||||
(click)="viewCompare(edit)"
|
||||
>
|
||||
<fa-icon icon="eye" class="m-r-10"></fa-icon>{{ loadingCompareFor === edit ? 'Loading…' : 'Compare' }}
|
||||
</button>
|
||||
<button mat-stroked-button class="psae-btn psae-btn-approve" (click)="approve(edit)">
|
||||
<fa-icon icon="check" class="m-r-10"></fa-icon>Approve
|
||||
@@ -84,67 +89,6 @@
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns" class="table-row"></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if (selectedEdit) {
|
||||
<div class="psae-diff-panel">
|
||||
<div class="psae-diff-header">
|
||||
<h3>Proposed Changes — Account "{{ selectedEdit.accountNo }}"</h3>
|
||||
<button mat-icon-button (click)="closeCompare()"><fa-icon icon="times"></fa-icon></button>
|
||||
</div>
|
||||
|
||||
@if (selectedEdit.facilityConfigSubmitted) {
|
||||
<div class="psae-diff-block">
|
||||
<h4>Digital & Account Features</h4>
|
||||
<table class="psae-diff-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Feature</th>
|
||||
<th>Proposed Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (row of entries(selectedEdit.proposedFacilityConfig); track row.key) {
|
||||
<tr>
|
||||
<td class="psae-diff-label">{{ row.key }}</td>
|
||||
<td class="psae-diff-proposed">{{ row.value }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (selectedEdit.nomineeSubmitted) {
|
||||
<div class="psae-diff-block">
|
||||
<h4>Nominee Change</h4>
|
||||
<table class="psae-diff-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Proposed Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (row of entries(selectedEdit.proposedNominee); track row.key) {
|
||||
<tr>
|
||||
<td class="psae-diff-label">{{ row.key }}</td>
|
||||
<td class="psae-diff-proposed">{{ row.value }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!selectedEdit.facilityConfigSubmitted && !selectedEdit.nomineeSubmitted) {
|
||||
<p class="psae-diff-loading">No proposed changes recorded on this edit.</p>
|
||||
}
|
||||
|
||||
@if (selectedEdit.rejectReason) {
|
||||
<p class="psae-reject-reason"><strong>Reject Reason:</strong> {{ selectedEdit.rejectReason }}</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@if (!showData) {
|
||||
|
||||
-73
@@ -107,79 +107,6 @@ $text-muted: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
.psae-diff-panel {
|
||||
margin-top: 20px;
|
||||
border: 1px solid $border;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.psae-diff-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.psae-diff-block {
|
||||
margin-top: 16px;
|
||||
|
||||
h4 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 12.5px;
|
||||
font-weight: 700;
|
||||
color: #0d1b2a;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
}
|
||||
|
||||
.psae-diff-loading {
|
||||
color: $text-muted;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.psae-diff-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 4px;
|
||||
font-size: 12.5px;
|
||||
|
||||
th,
|
||||
td {
|
||||
text-align: left;
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid $border;
|
||||
}
|
||||
|
||||
th {
|
||||
color: $text-muted;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.psae-diff-label {
|
||||
font-weight: 600;
|
||||
color: #0d1b2a;
|
||||
}
|
||||
|
||||
.psae-diff-proposed {
|
||||
color: #15803d;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.psae-reject-reason {
|
||||
margin-top: 14px;
|
||||
font-size: 12.5px;
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
+33
-13
@@ -32,6 +32,7 @@ import { ConfirmationDialogComponent } from 'app/shared/confirmation-dialog/conf
|
||||
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 { SavingsAccountEditCompareDialogComponent } from './savings-account-edit-compare-dialog/savings-account-edit-compare-dialog.component';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
/**
|
||||
@@ -79,7 +80,8 @@ export class PendingSavingsAccountEditsComponent {
|
||||
'actions'
|
||||
];
|
||||
|
||||
selectedEdit: any = null;
|
||||
/** Set only while the current account is being fetched, so the button can show progress. */
|
||||
loadingCompareFor: any = null;
|
||||
|
||||
constructor() {
|
||||
this.route.data.subscribe((data: { pendingSavingsAccountEditsData: any; pendingCurrentAccountEditsData: any }) => {
|
||||
@@ -120,18 +122,37 @@ export class PendingSavingsAccountEditsComponent {
|
||||
return isNaN(d.getTime()) ? String(date) : d.toLocaleString('en-IN');
|
||||
}
|
||||
|
||||
/** Readable list of "key: value" pairs for the proposed facility/nominee changes. */
|
||||
entries(obj: any): { key: string; value: any }[] {
|
||||
if (!obj) return [];
|
||||
return Object.keys(obj).map((key) => ({ key, value: obj[key] }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the account's current Digital & Account Feature config so the popup can show it next to
|
||||
* what's proposed, then opens the compare dialog. The maker's edit form already submits only the
|
||||
* toggles it actually changed (see EditSavingsAccountFeaturesComponent#buildFacilityDiff), so
|
||||
* proposedFacilityConfig only ever carries changed fields -- the dialog still diffs against this
|
||||
* current config defensively, so an unchanged value can never appear in the popup.
|
||||
*/
|
||||
viewCompare(edit: any): void {
|
||||
this.selectedEdit = this.selectedEdit === edit ? null : edit;
|
||||
}
|
||||
|
||||
closeCompare(): void {
|
||||
this.selectedEdit = null;
|
||||
this.loadingCompareFor = edit;
|
||||
const isCurrent = edit.accountType === 'Current';
|
||||
const account$ = isCurrent
|
||||
? this.currentAccountsService.getCurrentAccountAndTemplate(edit.currentAccountId, false)
|
||||
: this.savingsService.getSavingsAccountAndTemplate(edit.savingsAccountId, false);
|
||||
account$.subscribe({
|
||||
next: (account: any) => {
|
||||
this.loadingCompareFor = null;
|
||||
this.dialog.open(SavingsAccountEditCompareDialogComponent, {
|
||||
width: '760px',
|
||||
data: {
|
||||
accountNo: edit.accountNo,
|
||||
currentFacilityConfig: account?.accountFacilityConfig ?? {},
|
||||
proposedFacilityConfig: edit.proposedFacilityConfig ?? {},
|
||||
proposedNominee: edit.proposedNominee ?? {},
|
||||
rejectReason: edit.rejectReason
|
||||
}
|
||||
});
|
||||
},
|
||||
error: () => {
|
||||
this.loadingCompareFor = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
approve(edit: any): void {
|
||||
@@ -189,7 +210,6 @@ export class PendingSavingsAccountEditsComponent {
|
||||
}
|
||||
|
||||
reload(): void {
|
||||
this.closeCompare();
|
||||
const url = this.router.url;
|
||||
this.router
|
||||
.navigateByUrl('/checker-inbox-and-tasks', { skipLocationChange: true })
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
<!--
|
||||
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="saecd-title">
|
||||
<mat-icon class="saecd-title__icon">difference</mat-icon>
|
||||
<span>
|
||||
Proposed changes — <strong>{{ accountNo }}</strong>
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<div mat-dialog-content class="saecd-content">
|
||||
@if (facilityRows.length === 0 && nomineeRows.length === 0) {
|
||||
<div class="saecd-empty">
|
||||
<mat-icon class="saecd-empty__icon">check_circle</mat-icon>
|
||||
<p class="saecd-empty__title">No changes to review</p>
|
||||
<p class="saecd-empty__text">This edit was submitted without changing any feature or nominee value.</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (facilityRows.length > 0) {
|
||||
<h3 class="saecd-section">Digital & Account Features</h3>
|
||||
<table class="saecd-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Feature</th>
|
||||
<th>Current</th>
|
||||
<th>Proposed</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (row of facilityRows; track row.label) {
|
||||
<tr>
|
||||
<td class="saecd-table__label">{{ row.label }}</td>
|
||||
<td class="saecd-table__old">{{ row.current }}</td>
|
||||
<td class="saecd-table__new">{{ row.proposed }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
@if (nomineeRows.length > 0) {
|
||||
<h3 class="saecd-section">Nominee Change</h3>
|
||||
<table class="saecd-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Proposed</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (row of nomineeRows; track row.label) {
|
||||
<tr>
|
||||
<td class="saecd-table__label">{{ row.label }}</td>
|
||||
<td class="saecd-table__new" colspan="2">{{ row.proposed }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
@if (rejectReason) {
|
||||
<p class="saecd-reject-reason"><strong>Reject Reason:</strong> {{ rejectReason }}</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button mat-dialog-close>{{ 'labels.buttons.Close' | translate }}</button>
|
||||
</mat-dialog-actions>
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
$border: #e2e8f0;
|
||||
$muted: #64748b;
|
||||
|
||||
.saecd-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
border-bottom: 1px solid $border;
|
||||
padding-bottom: 12px;
|
||||
margin-bottom: 0;
|
||||
|
||||
&__icon {
|
||||
color: #0ea5e9;
|
||||
}
|
||||
}
|
||||
|
||||
.saecd-content {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.saecd-section {
|
||||
margin: 20px 0 10px;
|
||||
font-size: 12.5px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.4px;
|
||||
color: $muted;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.saecd-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12.5px;
|
||||
|
||||
th,
|
||||
td {
|
||||
text-align: left;
|
||||
padding: 9px 12px;
|
||||
border-bottom: 1px solid $border;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
th {
|
||||
color: $muted;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.4px;
|
||||
background: #f8fafc;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
tbody tr:hover {
|
||||
background: #fffbeb;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
width: 34%;
|
||||
}
|
||||
|
||||
&__old {
|
||||
color: #b91c1c;
|
||||
text-decoration: line-through;
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
&__new {
|
||||
color: #15803d;
|
||||
font-weight: 600;
|
||||
width: 33%;
|
||||
}
|
||||
}
|
||||
|
||||
.saecd-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 32px 16px;
|
||||
|
||||
&__icon {
|
||||
font-size: 40px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
color: #16a34a;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin: 0 0 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
&__text {
|
||||
margin: 0;
|
||||
font-size: 12.5px;
|
||||
color: $muted;
|
||||
max-width: 420px;
|
||||
}
|
||||
}
|
||||
|
||||
.saecd-reject-reason {
|
||||
margin: 20px 0 0;
|
||||
padding: 10px 12px;
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: 6px;
|
||||
font-size: 12.5px;
|
||||
color: #991b1b;
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* 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 {
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialogRef,
|
||||
MatDialogTitle,
|
||||
MatDialogContent,
|
||||
MatDialogActions,
|
||||
MatDialogClose
|
||||
} from '@angular/material/dialog';
|
||||
import { MatButton } from '@angular/material/button';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
interface DiffRow {
|
||||
label: string;
|
||||
current: string;
|
||||
proposed: string;
|
||||
}
|
||||
|
||||
/** Human-readable labels for the Digital & Account Feature toggles, matching the maker's edit form. */
|
||||
const FACILITY_LABELS: Record<string, string> = {
|
||||
atmFacility: 'ATM Facility',
|
||||
atmWithdrawalFacility: 'ATM Withdrawal',
|
||||
upiFacility: 'UPI Facility',
|
||||
posTransactionsFacility: 'POS Transactions',
|
||||
chequeBook: 'Cheque Book',
|
||||
impsFacility: 'IMPS Facility',
|
||||
smsAlerts: 'SMS Alerts',
|
||||
bbpsFacility: 'BBPS Facility',
|
||||
insuranceCover: 'Insurance Cover',
|
||||
bepgFacility: 'BEPG Facility',
|
||||
neftFacility: 'NEFT Facility',
|
||||
rtgsFacility: 'RTGS Facility',
|
||||
interBankTotalCapFacility: 'Inter-Bank Total Cap',
|
||||
withdrawalFacility: 'Withdrawal Facility',
|
||||
depositFacilityKyc: 'Deposit Facility (KYC)',
|
||||
virtualDebitCard: 'Virtual Debit Card'
|
||||
};
|
||||
|
||||
const NOMINEE_LABELS: Record<string, string> = {
|
||||
operation: 'Operation',
|
||||
nomineeId: 'Nominee Id',
|
||||
nomineeClientId: 'Nominee Client Id',
|
||||
relationshipId: 'Relationship Id',
|
||||
percentageShare: 'Percentage Share',
|
||||
isActive: 'Active',
|
||||
nomineeIsMinor: 'Nominee Is Minor',
|
||||
guardianClientId: 'Guardian Client Id',
|
||||
guardianRelationshipId: 'Guardian Relationship Id',
|
||||
effectiveDate: 'Effective Date',
|
||||
remarks: 'Remarks'
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows what a pending savings/current account Nominee & Digital-Feature edit actually changes, as a
|
||||
* popup instead of an inline panel. The maker's edit form already submits only the toggles that were
|
||||
* actually flipped (see EditSavingsAccountFeaturesComponent#buildFacilityDiff), so proposedFacilityConfig
|
||||
* only ever carries changed fields -- this still diffs against the account's current config defensively,
|
||||
* so an unchanged value can never appear here even if that ever stops being true upstream.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-savings-account-edit-compare-dialog',
|
||||
templateUrl: './savings-account-edit-compare-dialog.component.html',
|
||||
styleUrls: ['./savings-account-edit-compare-dialog.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatDialogTitle,
|
||||
MatDialogContent,
|
||||
MatDialogActions,
|
||||
MatDialogClose,
|
||||
MatButton,
|
||||
MatIcon
|
||||
]
|
||||
})
|
||||
export class SavingsAccountEditCompareDialogComponent implements OnInit {
|
||||
dialogRef = inject<MatDialogRef<SavingsAccountEditCompareDialogComponent>>(MatDialogRef);
|
||||
data = inject(MAT_DIALOG_DATA);
|
||||
|
||||
accountNo = '';
|
||||
facilityRows: DiffRow[] = [];
|
||||
nomineeRows: DiffRow[] = [];
|
||||
rejectReason: string | null = null;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.accountNo = this.data.accountNo ?? '';
|
||||
this.rejectReason = this.data.rejectReason ?? null;
|
||||
|
||||
const currentFacilityConfig = this.data.currentFacilityConfig ?? {};
|
||||
const proposedFacilityConfig = this.data.proposedFacilityConfig ?? {};
|
||||
this.facilityRows = Object.keys(proposedFacilityConfig)
|
||||
.filter((key) => !this.sameValue(currentFacilityConfig[key], proposedFacilityConfig[key]))
|
||||
.map((key) => ({
|
||||
label: FACILITY_LABELS[key] ?? this.humanizeKey(key),
|
||||
current: this.displayValue(currentFacilityConfig[key]),
|
||||
proposed: this.displayValue(proposedFacilityConfig[key])
|
||||
}))
|
||||
.sort((a, b) => a.label.localeCompare(b.label));
|
||||
|
||||
const proposedNominee = this.data.proposedNominee ?? {};
|
||||
this.nomineeRows = Object.keys(proposedNominee)
|
||||
.filter((key) => proposedNominee[key] !== null && proposedNominee[key] !== undefined)
|
||||
.map((key) => ({
|
||||
label: NOMINEE_LABELS[key] ?? this.humanizeKey(key),
|
||||
current: '—',
|
||||
proposed: this.displayValue(proposedNominee[key])
|
||||
}));
|
||||
}
|
||||
|
||||
private sameValue(a: any, b: any): boolean {
|
||||
if (a === undefined) {
|
||||
a = null;
|
||||
}
|
||||
if (b === undefined) {
|
||||
b = null;
|
||||
}
|
||||
return a === b;
|
||||
}
|
||||
|
||||
private displayValue(value: any): string {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '—';
|
||||
}
|
||||
if (typeof value === 'boolean') {
|
||||
return value ? 'Yes' : 'No';
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
/** camelCase field name to a spaced, capitalised label -- fallback for anything not in the label maps. */
|
||||
private humanizeKey(key: string): string {
|
||||
const spaced = key
|
||||
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
||||
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
|
||||
.trim();
|
||||
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
||||
}
|
||||
}
|
||||
@@ -199,6 +199,38 @@ export class TasksService {
|
||||
return this.http.post(`/currentaccounts/${accountId}`, data, { params: httpParams });
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate a savings account
|
||||
*/
|
||||
activateSavingsAccount(accountId: any, data: any): Observable<any> {
|
||||
const httpParams = new HttpParams().set('command', 'activate');
|
||||
return this.http.post(`/savingsaccounts/${accountId}`, data, { params: httpParams });
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate a fixed deposit account
|
||||
*/
|
||||
activateFDAccount(accountId: any, data: any): Observable<any> {
|
||||
const httpParams = new HttpParams().set('command', 'activate');
|
||||
return this.http.post(`/fixeddepositaccounts/${accountId}`, data, { params: httpParams });
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate a recurring deposit account
|
||||
*/
|
||||
activateRDAccount(accountId: any, data: any): Observable<any> {
|
||||
const httpParams = new HttpParams().set('command', 'activate');
|
||||
return this.http.post(`/recurringdepositaccounts/${accountId}`, data, { params: httpParams });
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate a current account
|
||||
*/
|
||||
activateCurrentAccount(accountId: any, data: any): Observable<any> {
|
||||
const httpParams = new HttpParams().set('command', 'activate');
|
||||
return this.http.post(`/currentaccounts/${accountId}`, data, { params: httpParams });
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject a savings account
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user