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:
@@ -778,23 +778,33 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
|||||||
loadingAccount = false;
|
loadingAccount = false;
|
||||||
accountError = '';
|
accountError = '';
|
||||||
|
|
||||||
/** Share redemption settlement is always a book-transfer to the linked savings account — never a
|
/**
|
||||||
* cash handover. A Share (SHR) row's own TXN TP. never offers a cash payment type, and neither
|
* A teller only ever picks Cash or Transfer — which specific internal settlement it actually is
|
||||||
* does its paired CR (savings) row in "Enter Bank Transfer" mode — that CR row's payment type is
|
* (a share redemption, a dividend payout, a GL-to-GL or GL-to-bank book entry) is business logic
|
||||||
* the one that actually governs how the settlement credit posts (see submitShareRedemptionTransfer()).
|
* the backend already threads through from whichever payment type the clerk picked here (see
|
||||||
* Every other account type/mode keeps the full, unfiltered list. */
|
* ClerkShareWithdrawalRequestApiResource#creditLinkedSavingsAccount on the backend), not a separate
|
||||||
|
* teller-facing choice. System-defined payment types (Share Redemption Transfer, Dividend Claim
|
||||||
|
* Settlement, the loan repayment-adjustment types, ...) exist purely as internal audit labels and
|
||||||
|
* are never offered here.
|
||||||
|
*
|
||||||
|
* Share redemption settlement is always a book-transfer to the linked savings account — never a
|
||||||
|
* cash handover. A Share (SHR) row's own TXN TP. never offers a cash payment type, and neither
|
||||||
|
* does its paired CR (savings) row in "Enter Bank Transfer" mode — that CR row's payment type is
|
||||||
|
* the one that actually governs how the settlement credit posts (see submitShareRedemptionTransfer()).
|
||||||
|
*/
|
||||||
paymentTypesForRow(row: TxnRow): any[] {
|
paymentTypesForRow(row: TxnRow): any[] {
|
||||||
|
const tellerFacing = this.paymentTypes.filter((pt: any) => !pt.isSystemDefined);
|
||||||
if (row.acTyp === 'SHR') {
|
if (row.acTyp === 'SHR') {
|
||||||
return this.paymentTypes.filter((pt: any) => !pt.isCashPayment);
|
return tellerFacing.filter((pt: any) => !pt.isCashPayment);
|
||||||
}
|
}
|
||||||
const isShareSettlementCredit =
|
const isShareSettlementCredit =
|
||||||
row.typ === 'Enter Bank Transfer' &&
|
row.typ === 'Enter Bank Transfer' &&
|
||||||
row.dbCr === 'Cr' &&
|
row.dbCr === 'Cr' &&
|
||||||
this.rows.some((r) => r.typ === 'Enter Bank Transfer' && r.dbCr === 'Dr' && r.acTyp === 'SHR');
|
this.rows.some((r) => r.typ === 'Enter Bank Transfer' && r.dbCr === 'Dr' && r.acTyp === 'SHR');
|
||||||
if (isShareSettlementCredit) {
|
if (isShareSettlementCredit) {
|
||||||
return this.paymentTypes.filter((pt: any) => !pt.isCashPayment);
|
return tellerFacing.filter((pt: any) => !pt.isCashPayment);
|
||||||
}
|
}
|
||||||
return this.paymentTypes;
|
return tellerFacing;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Per-row account id/name map: acNo → Fineract id/name (used for multi-row disbursement)
|
// Per-row account id/name map: acNo → Fineract id/name (used for multi-row disbursement)
|
||||||
@@ -1563,7 +1573,7 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
|||||||
if (settlementPaymentType?.isCashPayment) {
|
if (settlementPaymentType?.isCashPayment) {
|
||||||
this.showAlert('error', 'Cash Not Allowed for Share Transactions', [
|
this.showAlert('error', 'Cash Not Allowed for Share Transactions', [
|
||||||
'Share redemption settlement is always a book-transfer to the linked savings account, never cash.',
|
'Share redemption settlement is always a book-transfer to the linked savings account, never cash.',
|
||||||
'Select a non-cash Payment Type on the CR row (e.g. Transfer or Share Redemption Transfer).'
|
'Select Transfer as the Payment Type on the CR row.'
|
||||||
]);
|
]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
<!--
|
||||||
|
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>{{ 'labels.heading.Edit Facility Charge Mapping' | translate }}</h2>
|
||||||
|
|
||||||
|
<mat-dialog-content>
|
||||||
|
<form [formGroup]="editMappingForm" class="efcm-form">
|
||||||
|
<mat-form-field appearance="outline" class="efcm-field">
|
||||||
|
<mat-label>{{ 'labels.inputs.Charge' | translate }}</mat-label>
|
||||||
|
<mat-select required formControlName="chargeId">
|
||||||
|
@for (charge of data.chargeOptions; track charge.id) {
|
||||||
|
<mat-option [value]="charge.id">{{ charge.name }} ({{ charge.amount }})</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
@if (editMappingForm.controls.chargeId.hasError('required')) {
|
||||||
|
<mat-error>
|
||||||
|
{{ 'labels.inputs.Charge' | translate }} {{ 'labels.commons.is' | translate }}
|
||||||
|
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<div class="efcm-amount-readonly">
|
||||||
|
<span class="efcm-amount-readonly__label">{{ 'labels.inputs.Amount' | translate }}</span>
|
||||||
|
<span class="efcm-amount-readonly__value">
|
||||||
|
{{ selectedChargeAmount !== null ? selectedChargeAmount : '-' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="efcm-checkbox-row">
|
||||||
|
<mat-checkbox formControlName="isActive" labelPosition="after">
|
||||||
|
{{ 'labels.inputs.Active' | translate }}
|
||||||
|
</mat-checkbox>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</mat-dialog-content>
|
||||||
|
|
||||||
|
<mat-dialog-actions align="end">
|
||||||
|
<button mat-stroked-button mat-dialog-close>{{ 'labels.buttons.Cancel' | translate }}</button>
|
||||||
|
<button
|
||||||
|
mat-flat-button
|
||||||
|
color="primary"
|
||||||
|
[disabled]="!editMappingForm.valid || editMappingForm.pristine"
|
||||||
|
(click)="submit()"
|
||||||
|
>
|
||||||
|
{{ 'labels.buttons.Submit' | translate }}
|
||||||
|
</button>
|
||||||
|
</mat-dialog-actions>
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* 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/.
|
||||||
|
*/
|
||||||
|
.efcm-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 340px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.efcm-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.efcm-checkbox-row {
|
||||||
|
margin: 4px 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.efcm-amount-readonly {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 0 12px;
|
||||||
|
color: rgb(0 0 0 / 60%);
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__value {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgb(0 0 0 / 87%);
|
||||||
|
}
|
||||||
|
}
|
||||||
+71
@@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
MAT_DIALOG_DATA,
|
||||||
|
MatDialogRef,
|
||||||
|
MatDialogTitle,
|
||||||
|
MatDialogContent,
|
||||||
|
MatDialogActions,
|
||||||
|
MatDialogClose
|
||||||
|
} from '@angular/material/dialog';
|
||||||
|
import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms';
|
||||||
|
import { CdkScrollable } from '@angular/cdk/scrolling';
|
||||||
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lets an admin change which charge a single facility (e.g. ATM_FACILITY) maps to. The amount charged is
|
||||||
|
* always the selected charge's own, live amount (see FacilityChargeAutoApplyService, which reads
|
||||||
|
* charge.getAmount() when applying it) -- shown here read-only for reference, not independently editable.
|
||||||
|
* To change the amount, edit the Charge itself under Products > Charges.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'mifosx-edit-facility-charge-mapping-dialog',
|
||||||
|
templateUrl: './edit-facility-charge-mapping-dialog.component.html',
|
||||||
|
styleUrls: ['./edit-facility-charge-mapping-dialog.component.scss'],
|
||||||
|
imports: [
|
||||||
|
...STANDALONE_SHARED_IMPORTS,
|
||||||
|
MatDialogTitle,
|
||||||
|
CdkScrollable,
|
||||||
|
MatDialogContent,
|
||||||
|
MatDialogActions,
|
||||||
|
MatDialogClose
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class EditFacilityChargeMappingDialogComponent implements OnInit {
|
||||||
|
dialogRef = inject<MatDialogRef<EditFacilityChargeMappingDialogComponent>>(MatDialogRef);
|
||||||
|
private formBuilder = inject(UntypedFormBuilder);
|
||||||
|
data = inject(MAT_DIALOG_DATA);
|
||||||
|
|
||||||
|
editMappingForm: UntypedFormGroup;
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.editMappingForm = this.formBuilder.group({
|
||||||
|
chargeId: [
|
||||||
|
this.data.mapping.chargeId,
|
||||||
|
Validators.required
|
||||||
|
],
|
||||||
|
// A facility with no charge mapped yet is being configured for the first time here -- default
|
||||||
|
// Active to checked so picking a charge and submitting is enough to make it usable immediately.
|
||||||
|
isActive: [this.data.mapping.chargeId ? this.data.mapping.isActive : true]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Amount of whichever charge is currently selected in the form -- updates live as the selection changes. */
|
||||||
|
get selectedChargeAmount(): number | null {
|
||||||
|
const selectedId = this.editMappingForm?.get('chargeId')?.value;
|
||||||
|
const selected = (this.data.chargeOptions || []).find((charge: any) => charge.id === selectedId);
|
||||||
|
return selected ? selected.amount : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
submit(): void {
|
||||||
|
this.dialogRef.close(this.editMappingForm.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<!--
|
||||||
|
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="fcm-header">
|
||||||
|
<h1>{{ 'labels.heading.Facility Charge Mappings' | translate }}</h1>
|
||||||
|
<p class="fcm-header__sub">
|
||||||
|
{{
|
||||||
|
'labels.text.Which charge is applied when a facility (ATM, UPI, Cheque Book, ...) is enabled on an account'
|
||||||
|
| translate
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mat-elevation-z8 container">
|
||||||
|
<table mat-table [dataSource]="dataSource" matSort>
|
||||||
|
<ng-container matColumnDef="facilityType">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Facility' | translate }}</th>
|
||||||
|
<td mat-cell *matCellDef="let mapping">{{ facilityLabel(mapping.facilityType) }}</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="chargeName">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Mapped Charge' | translate }}</th>
|
||||||
|
<td mat-cell *matCellDef="let mapping">
|
||||||
|
{{ mapping.chargeName || '—' }}
|
||||||
|
@if (mapping.chargeId && !mapping.chargeAvailable) {
|
||||||
|
<span
|
||||||
|
class="fcm-broken-badge"
|
||||||
|
[matTooltip]="
|
||||||
|
'labels.text.This charge has been deleted or deactivated. Update the mapping to avoid errors when this facility is enabled.'
|
||||||
|
| translate
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
{{ 'labels.text.Deleted' | translate }}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="defaultAmount">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Amount' | translate }}</th>
|
||||||
|
<td mat-cell *matCellDef="let mapping">{{ mapping.defaultAmount !== null ? mapping.defaultAmount : '—' }}</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="isActive">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Active' | translate }}</th>
|
||||||
|
<td mat-cell *matCellDef="let mapping">
|
||||||
|
{{ (mapping.isActive ? 'labels.commons.Yes' : 'labels.commons.No') | translate }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="actions">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
|
||||||
|
<td mat-cell *matCellDef="let mapping">
|
||||||
|
<button
|
||||||
|
mat-stroked-button
|
||||||
|
color="primary"
|
||||||
|
*mifosxHasPermission="'UPDATE_FACILITYCHARGEMAPPING'"
|
||||||
|
(click)="editMapping(mapping)"
|
||||||
|
>
|
||||||
|
{{ 'labels.buttons.Edit' | translate }}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||||
|
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<mat-paginator [pageSizeOptions]="[10, 25]" showFirstLastButtons></mat-paginator>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* 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/.
|
||||||
|
*/
|
||||||
|
.fcm-header {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__sub {
|
||||||
|
margin: 0;
|
||||||
|
color: rgb(0 0 0 / 60%);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fcm-broken-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
margin-left: 8px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgb(211 47 47 / 10%);
|
||||||
|
color: #d32f2f;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
/**
|
||||||
|
* 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, ViewChild, inject } from '@angular/core';
|
||||||
|
import { MatPaginator } from '@angular/material/paginator';
|
||||||
|
import { MatSort, MatSortHeader } from '@angular/material/sort';
|
||||||
|
import {
|
||||||
|
MatTableDataSource,
|
||||||
|
MatTable,
|
||||||
|
MatColumnDef,
|
||||||
|
MatHeaderCellDef,
|
||||||
|
MatHeaderCell,
|
||||||
|
MatCellDef,
|
||||||
|
MatCell,
|
||||||
|
MatHeaderRowDef,
|
||||||
|
MatHeaderRow,
|
||||||
|
MatRowDef,
|
||||||
|
MatRow
|
||||||
|
} from '@angular/material/table';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { MatTooltip } from '@angular/material/tooltip';
|
||||||
|
|
||||||
|
/** rxjs Imports */
|
||||||
|
import { EMPTY, Observable } from 'rxjs';
|
||||||
|
import { switchMap } from 'rxjs/operators';
|
||||||
|
|
||||||
|
/** Custom Services */
|
||||||
|
import { ProductsService } from '../products.service';
|
||||||
|
import { AlertService } from 'app/core/alert/alert.service';
|
||||||
|
import { EditFacilityChargeMappingDialogComponent } from './edit-facility-charge-mapping-dialog/edit-facility-charge-mapping-dialog.component';
|
||||||
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists which charge is applied when each of the 16 named account facilities (ATM Facility, UPI
|
||||||
|
* Facility, Cheque Book, ...) is enabled on a savings/current account, and lets an admin change it.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'mifosx-facility-charge-mappings',
|
||||||
|
templateUrl: './facility-charge-mappings.component.html',
|
||||||
|
styleUrls: ['./facility-charge-mappings.component.scss'],
|
||||||
|
imports: [
|
||||||
|
...STANDALONE_SHARED_IMPORTS,
|
||||||
|
MatTable,
|
||||||
|
MatSort,
|
||||||
|
MatColumnDef,
|
||||||
|
MatHeaderCellDef,
|
||||||
|
MatHeaderCell,
|
||||||
|
MatSortHeader,
|
||||||
|
MatCellDef,
|
||||||
|
MatCell,
|
||||||
|
MatHeaderRowDef,
|
||||||
|
MatHeaderRow,
|
||||||
|
MatRowDef,
|
||||||
|
MatRow,
|
||||||
|
MatPaginator,
|
||||||
|
MatTooltip
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class FacilityChargeMappingsComponent implements OnInit {
|
||||||
|
private route = inject(ActivatedRoute);
|
||||||
|
private productsService = inject(ProductsService);
|
||||||
|
private dialog = inject(MatDialog);
|
||||||
|
private alertService = inject(AlertService);
|
||||||
|
|
||||||
|
facilityChargeMappingsData: any[] = [];
|
||||||
|
displayedColumns: string[] = [
|
||||||
|
'facilityType',
|
||||||
|
'chargeName',
|
||||||
|
'defaultAmount',
|
||||||
|
'isActive',
|
||||||
|
'actions'
|
||||||
|
];
|
||||||
|
dataSource: MatTableDataSource<any>;
|
||||||
|
|
||||||
|
@ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
|
||||||
|
@ViewChild(MatSort, { static: true }) sort: MatSort;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.route.data.subscribe((data: { facilityChargeMappings: any }) => {
|
||||||
|
this.facilityChargeMappingsData = data.facilityChargeMappings;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.setFacilityChargeMappings();
|
||||||
|
}
|
||||||
|
|
||||||
|
setFacilityChargeMappings() {
|
||||||
|
this.dataSource = new MatTableDataSource(this.facilityChargeMappingsData);
|
||||||
|
this.dataSource.paginator = this.paginator;
|
||||||
|
this.dataSource.sort = this.sort;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Human-readable label for a facility_type code, e.g. ATM_FACILITY -> ATM Facility. */
|
||||||
|
facilityLabel(facilityType: string): string {
|
||||||
|
if (!facilityType) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return facilityType
|
||||||
|
.toLowerCase()
|
||||||
|
.split('_')
|
||||||
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
|
.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
editMapping(mapping: any): void {
|
||||||
|
this.productsService
|
||||||
|
.getFacilityChargeMappingsTemplate()
|
||||||
|
.pipe(
|
||||||
|
switchMap((template: any) =>
|
||||||
|
this.dialog
|
||||||
|
.open(EditFacilityChargeMappingDialogComponent, {
|
||||||
|
data: { mapping, chargeOptions: template.chargeOptions || [] }
|
||||||
|
})
|
||||||
|
.afterClosed()
|
||||||
|
),
|
||||||
|
switchMap((formValue: any): Observable<any> => {
|
||||||
|
if (!formValue) {
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
return this.productsService.updateFacilityChargeMapping(mapping.id, formValue);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe((result: any) => {
|
||||||
|
if (result) {
|
||||||
|
this.alertService.alert({
|
||||||
|
type: 'Facility Charge Mapping',
|
||||||
|
message: `${this.facilityLabel(mapping.facilityType)} mapping updated successfully.`
|
||||||
|
});
|
||||||
|
this.productsService.getFacilityChargeMappings().subscribe((mappings: any) => {
|
||||||
|
this.facilityChargeMappingsData = mappings;
|
||||||
|
this.setFacilityChargeMappings();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* 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 { Injectable, inject } from '@angular/core';
|
||||||
|
|
||||||
|
/** rxjs Imports */
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
/** Custom Services */
|
||||||
|
import { ProductsService } from '../products.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Facility charge mappings data resolver.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class FacilityChargeMappingsResolver {
|
||||||
|
private productsService = inject(ProductsService);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all 16 facility -> charge mappings.
|
||||||
|
* @returns {Observable<any>}
|
||||||
|
*/
|
||||||
|
resolve(): Observable<any> {
|
||||||
|
return this.productsService.getFacilityChargeMappings();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ import { ViewRecurringDepositProductComponent } from './recurring-deposit-produc
|
|||||||
import { ChargesComponent } from './charges/charges.component';
|
import { ChargesComponent } from './charges/charges.component';
|
||||||
import { ViewChargeComponent } from './charges/view-charge/view-charge.component';
|
import { ViewChargeComponent } from './charges/view-charge/view-charge.component';
|
||||||
import { CreateChargeComponent } from './charges/create-charge/create-charge.component';
|
import { CreateChargeComponent } from './charges/create-charge/create-charge.component';
|
||||||
|
import { FacilityChargeMappingsComponent } from './facility-charge-mappings/facility-charge-mappings.component';
|
||||||
import { FixedDepositProductsComponent } from './fixed-deposit-products/fixed-deposit-products.component';
|
import { FixedDepositProductsComponent } from './fixed-deposit-products/fixed-deposit-products.component';
|
||||||
import { CreateFixedDepositProductComponent } from './fixed-deposit-products/create-fixed-deposit-product/create-fixed-deposit-product.component';
|
import { CreateFixedDepositProductComponent } from './fixed-deposit-products/create-fixed-deposit-product/create-fixed-deposit-product.component';
|
||||||
import { ProductsMixComponent } from './products-mix/products-mix.component';
|
import { ProductsMixComponent } from './products-mix/products-mix.component';
|
||||||
@@ -76,6 +77,7 @@ import { ShareProductAndTemplateResolver } from './share-products/edit-share-pro
|
|||||||
import { RecurringDepositProductsResolver } from './recurring-deposit-products/recurring-deposit-products.resolver';
|
import { RecurringDepositProductsResolver } from './recurring-deposit-products/recurring-deposit-products.resolver';
|
||||||
import { RecurringDepositProductResolver } from './recurring-deposit-products/recurring-deposit-product.resolver';
|
import { RecurringDepositProductResolver } from './recurring-deposit-products/recurring-deposit-product.resolver';
|
||||||
import { ChargesResolver } from './charges/charges.resolver';
|
import { ChargesResolver } from './charges/charges.resolver';
|
||||||
|
import { FacilityChargeMappingsResolver } from './facility-charge-mappings/facility-charge-mappings.resolver';
|
||||||
import { ChargeResolver } from './charges/charge.resolver';
|
import { ChargeResolver } from './charges/charge.resolver';
|
||||||
import { ChargesTemplateResolver } from './charges/charges-template.resolver';
|
import { ChargesTemplateResolver } from './charges/charges-template.resolver';
|
||||||
import { FixedDepositProductsResolver } from './fixed-deposit-products/fixed-deposit-products.resolver';
|
import { FixedDepositProductsResolver } from './fixed-deposit-products/fixed-deposit-products.resolver';
|
||||||
@@ -937,6 +939,14 @@ const routes: Routes = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'facility-charge-mappings',
|
||||||
|
data: { title: 'Facility Charge Mappings', breadcrumb: 'Facility Charge Mappings' },
|
||||||
|
component: FacilityChargeMappingsComponent,
|
||||||
|
resolve: {
|
||||||
|
facilityChargeMappings: FacilityChargeMappingsResolver
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'collaterals',
|
path: 'collaterals',
|
||||||
data: { title: 'Collaterals', breadcrumb: 'Collaterals' },
|
data: { title: 'Collaterals', breadcrumb: 'Collaterals' },
|
||||||
@@ -1015,6 +1025,7 @@ const routes: Routes = [
|
|||||||
ChargeResolver,
|
ChargeResolver,
|
||||||
ChargesTemplateAndResolver,
|
ChargesTemplateAndResolver,
|
||||||
ChargesTemplateResolver,
|
ChargesTemplateResolver,
|
||||||
|
FacilityChargeMappingsResolver,
|
||||||
FixedDepositProductsResolver,
|
FixedDepositProductsResolver,
|
||||||
FixedDepositProductsTemplateResolver,
|
FixedDepositProductsTemplateResolver,
|
||||||
ProductsMixResolver,
|
ProductsMixResolver,
|
||||||
|
|||||||
@@ -151,6 +151,22 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<a class="pd-card" [routerLink]="['facility-charge-mappings']" *mifosxHasPermission="'READ_FACILITYCHARGEMAPPING'">
|
||||||
|
<div class="pd-card__icon pd-card__icon--orange">
|
||||||
|
<i class="fas fa-sliders-h"></i>
|
||||||
|
</div>
|
||||||
|
<div class="pd-card__body">
|
||||||
|
<h3 class="pd-card__title">{{ 'labels.heading.Facility Charge Mappings' | translate }}</h3>
|
||||||
|
<p class="pd-card__desc">
|
||||||
|
{{
|
||||||
|
'labels.text.Choose which charge applies when an account facility (ATM, UPI, Cheque Book, ...) is enabled'
|
||||||
|
| translate
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<i class="fas fa-chevron-right pd-card__arrow"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
<a class="pd-card" [routerLink]="['collaterals']">
|
<a class="pd-card" [routerLink]="['collaterals']">
|
||||||
<div class="pd-card__icon pd-card__icon--slate">
|
<div class="pd-card__icon pd-card__icon--slate">
|
||||||
<i class="fas fa-shield-alt"></i>
|
<i class="fas fa-shield-alt"></i>
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ import { CreateLoanProductComponent } from './loan-products/create-loan-product/
|
|||||||
import { ManageTaxConfigurationsComponent } from './manage-tax-configurations/manage-tax-configurations.component';
|
import { ManageTaxConfigurationsComponent } from './manage-tax-configurations/manage-tax-configurations.component';
|
||||||
import { RecurringDepositProductsComponent } from './recurring-deposit-products/recurring-deposit-products.component';
|
import { RecurringDepositProductsComponent } from './recurring-deposit-products/recurring-deposit-products.component';
|
||||||
import { ChargesComponent } from './charges/charges.component';
|
import { ChargesComponent } from './charges/charges.component';
|
||||||
|
import { FacilityChargeMappingsComponent } from './facility-charge-mappings/facility-charge-mappings.component';
|
||||||
|
import { EditFacilityChargeMappingDialogComponent } from './facility-charge-mappings/edit-facility-charge-mapping-dialog/edit-facility-charge-mapping-dialog.component';
|
||||||
import { ViewChargeComponent } from './charges/view-charge/view-charge.component';
|
import { ViewChargeComponent } from './charges/view-charge/view-charge.component';
|
||||||
import { FixedDepositProductsComponent } from './fixed-deposit-products/fixed-deposit-products.component';
|
import { FixedDepositProductsComponent } from './fixed-deposit-products/fixed-deposit-products.component';
|
||||||
import { ProductsMixComponent } from './products-mix/products-mix.component';
|
import { ProductsMixComponent } from './products-mix/products-mix.component';
|
||||||
@@ -159,6 +161,8 @@ import { AdvancedAccountingMappingRuleComponent } from './loan-products/loan-pro
|
|||||||
RecurringDepositProductsComponent,
|
RecurringDepositProductsComponent,
|
||||||
ChargesComponent,
|
ChargesComponent,
|
||||||
ViewChargeComponent,
|
ViewChargeComponent,
|
||||||
|
FacilityChargeMappingsComponent,
|
||||||
|
EditFacilityChargeMappingDialogComponent,
|
||||||
FixedDepositProductsComponent,
|
FixedDepositProductsComponent,
|
||||||
ManageTaxComponentsComponent,
|
ManageTaxComponentsComponent,
|
||||||
ProductsMixComponent,
|
ProductsMixComponent,
|
||||||
|
|||||||
@@ -347,6 +347,29 @@ export class ProductsService {
|
|||||||
return this.http.get('/charges/template');
|
return this.http.get('/charges/template');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Observable<any>} All 16 facility -> charge mappings.
|
||||||
|
*/
|
||||||
|
getFacilityChargeMappings(): Observable<any> {
|
||||||
|
return this.http.get('/facilitychargemappings');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Observable<any>} Charge options available to map a facility to.
|
||||||
|
*/
|
||||||
|
getFacilityChargeMappingsTemplate(): Observable<any> {
|
||||||
|
return this.http.get('/facilitychargemappings/template');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} mappingId Facility charge mapping ID to update.
|
||||||
|
* @param {any} mapping New chargeId (and optionally defaultAmount/isActive).
|
||||||
|
* @returns {Observable<any>}
|
||||||
|
*/
|
||||||
|
updateFacilityChargeMapping(mappingId: string, mapping: any): Observable<any> {
|
||||||
|
return this.http.put(`/facilitychargemappings/${mappingId}`, mapping);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} chargeId Charge ID of charge.
|
* @param {string} chargeId Charge ID of charge.
|
||||||
* @returns {Observable<any>} Charge.
|
* @returns {Observable<any>} Charge.
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { AccountsService } from 'app/accounts/accounts.service';
|
|||||||
import { SettingsService } from 'app/settings/settings.service';
|
import { SettingsService } from 'app/settings/settings.service';
|
||||||
import { Dates } from 'app/core/utils/dates';
|
import { Dates } from 'app/core/utils/dates';
|
||||||
import { AuthenticationService } from '../../core/authentication/authentication.service';
|
import { AuthenticationService } from '../../core/authentication/authentication.service';
|
||||||
|
import { AlertService } from 'app/core/alert/alert.service';
|
||||||
|
|
||||||
/** Environment Configuration */
|
/** Environment Configuration */
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
@@ -85,6 +86,7 @@ export class SavingsAccountViewComponent implements OnInit {
|
|||||||
private settingsService = inject(SettingsService);
|
private settingsService = inject(SettingsService);
|
||||||
private dateUtils = inject(Dates);
|
private dateUtils = inject(Dates);
|
||||||
private authenticationService = inject(AuthenticationService);
|
private authenticationService = inject(AuthenticationService);
|
||||||
|
private alertService = inject(AlertService);
|
||||||
dialog = inject(MatDialog);
|
dialog = inject(MatDialog);
|
||||||
|
|
||||||
/** Savings Account Data */
|
/** Savings Account Data */
|
||||||
@@ -225,12 +227,8 @@ export class SavingsAccountViewComponent implements OnInit {
|
|||||||
this.isActive = status === 'Active';
|
this.isActive = status === 'Active';
|
||||||
const subStatus = this.savingsAccountData.subStatus;
|
const subStatus = this.savingsAccountData.subStatus;
|
||||||
this.buttonConfig = new SavingsButtonsConfiguration(status, subStatus);
|
this.buttonConfig = new SavingsButtonsConfiguration(status, subStatus);
|
||||||
if (this.savingsAccountData.clientId) {
|
// Transfer Funds is temporarily disabled for savings accounts (pending and active alike) --
|
||||||
this.buttonConfig.addOption({
|
// deliberately not added to the menu here; see the guarded 'Transfer Funds' switch case below.
|
||||||
name: 'Transfer Funds',
|
|
||||||
taskPermissionName: 'CREATE_ACCOUNTTRANSFER'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (this.savingsAccountData.externalId && environment.mifosInterbankTransfersEnabled) {
|
if (this.savingsAccountData.externalId && environment.mifosInterbankTransfersEnabled) {
|
||||||
this.buttonConfig.addOption({
|
this.buttonConfig.addOption({
|
||||||
name: 'Interbank Transfer',
|
name: 'Interbank Transfer',
|
||||||
@@ -347,15 +345,11 @@ export class SavingsAccountViewComponent implements OnInit {
|
|||||||
this.disableWithHoldTax();
|
this.disableWithHoldTax();
|
||||||
break;
|
break;
|
||||||
case 'Transfer Funds':
|
case 'Transfer Funds':
|
||||||
const queryParams: any = {
|
// Temporarily disabled for savings accounts -- blocked here too, not just left out of the
|
||||||
interbank: false,
|
// menu, so a stale menu render or direct invocation can't reach the transfer screen.
|
||||||
savingsId: this.savingsAccountData.id,
|
this.alertService.alert({
|
||||||
accountType: 'fromsavings'
|
type: 'Transfer Funds',
|
||||||
};
|
message: 'Transfer Funds is temporarily unavailable for savings accounts.'
|
||||||
this.router.navigate(['transfer-funds/make-account-transfer'], {
|
|
||||||
relativeTo: this.route,
|
|
||||||
queryParams: queryParams,
|
|
||||||
state: { balance: this.savingsAccountData.summary.availableBalance }
|
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'Interbank Transfer': {
|
case 'Interbank Transfer': {
|
||||||
|
|||||||
@@ -189,10 +189,6 @@ export class SavingsButtonsConfiguration {
|
|||||||
name: 'Withdrawn by Client',
|
name: 'Withdrawn by Client',
|
||||||
taskPermissionName: 'WITHDRAW_SAVINGSACCOUNT'
|
taskPermissionName: 'WITHDRAW_SAVINGSACCOUNT'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'Add Charge',
|
|
||||||
taskPermissionName: 'CREATE_SAVINGSACCOUNTCHARGE'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'Delete',
|
name: 'Delete',
|
||||||
taskPermissionName: 'DELETE_SAVINGSACCOUNT'
|
taskPermissionName: 'DELETE_SAVINGSACCOUNT'
|
||||||
|
|||||||
+60
-24
@@ -9,6 +9,7 @@
|
|||||||
import { Component, inject } from '@angular/core';
|
import { Component, inject } from '@angular/core';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { SelectionModel } from '@angular/cdk/collections';
|
import { SelectionModel } from '@angular/cdk/collections';
|
||||||
|
import { switchMap } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
MatTableDataSource,
|
MatTableDataSource,
|
||||||
MatTable,
|
MatTable,
|
||||||
@@ -141,7 +142,8 @@ export class PendingAccountComponent {
|
|||||||
data: {
|
data: {
|
||||||
title: 'Enter Approval Date',
|
title: 'Enter Approval Date',
|
||||||
layout: { addButtonText: 'Confirm' },
|
layout: { addButtonText: 'Confirm' },
|
||||||
formfields
|
formfields,
|
||||||
|
pristine: false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
dialogRef.afterClosed().subscribe((response: any) => {
|
dialogRef.afterClosed().subscribe((response: any) => {
|
||||||
@@ -166,7 +168,8 @@ export class PendingAccountComponent {
|
|||||||
data: {
|
data: {
|
||||||
title: 'Enter Rejection Date',
|
title: 'Enter Rejection Date',
|
||||||
layout: { addButtonText: 'Confirm' },
|
layout: { addButtonText: 'Confirm' },
|
||||||
formfields
|
formfields,
|
||||||
|
pristine: false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
dialogRef.afterClosed().subscribe((response: any) => {
|
dialogRef.afterClosed().subscribe((response: any) => {
|
||||||
@@ -183,7 +186,7 @@ export class PendingAccountComponent {
|
|||||||
const payload = { rejectedOnDate, dateFormat, locale };
|
const payload = { rejectedOnDate, dateFormat, locale };
|
||||||
|
|
||||||
const selected = this.selection.selected;
|
const selected = this.selection.selected;
|
||||||
let done = 0;
|
let settled = 0;
|
||||||
selected.forEach((account: any) => {
|
selected.forEach((account: any) => {
|
||||||
const depositTypeId = account.depositType?.id;
|
const depositTypeId = account.depositType?.id;
|
||||||
let reject$;
|
let reject$;
|
||||||
@@ -196,39 +199,72 @@ export class PendingAccountComponent {
|
|||||||
} else {
|
} else {
|
||||||
reject$ = this.tasksService.rejectSavingsAccount(account.id, payload);
|
reject$ = this.tasksService.rejectSavingsAccount(account.id, payload);
|
||||||
}
|
}
|
||||||
reject$.subscribe(() => {
|
reject$.subscribe({
|
||||||
done++;
|
next: () => {
|
||||||
if (done === selected.length) {
|
settled++;
|
||||||
this.reload();
|
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) {
|
private bulkApprove(submittedData: any) {
|
||||||
const dateFormat = this.settingsService.dateFormat;
|
const dateFormat = this.settingsService.dateFormat;
|
||||||
const locale = this.settingsService.language.code;
|
const locale = this.settingsService.language.code;
|
||||||
const approvedOnDate = this.dateUtils.formatDate(submittedData.value.approvedOnDate, dateFormat);
|
const approvedOnDate = this.dateUtils.formatDate(submittedData.value.approvedOnDate, dateFormat);
|
||||||
const payload = { approvedOnDate, dateFormat, locale };
|
|
||||||
|
|
||||||
const selected = this.selection.selected;
|
const selected = this.selection.selected;
|
||||||
let done = 0;
|
let settled = 0;
|
||||||
selected.forEach((account: any) => {
|
selected.forEach((account: any) => {
|
||||||
const depositTypeId = account.depositType?.id;
|
this.approveAndActivate$(account, approvedOnDate, dateFormat, locale).subscribe({
|
||||||
let approve$;
|
next: () => {
|
||||||
if (account.accountType === 'Current') {
|
settled++;
|
||||||
approve$ = this.tasksService.approveCurrentAccount(account.id, payload);
|
if (settled === selected.length) {
|
||||||
} else if (depositTypeId === 200) {
|
this.reload();
|
||||||
approve$ = this.tasksService.approveFDAccount(account.id, payload);
|
}
|
||||||
} else if (depositTypeId === 300) {
|
},
|
||||||
approve$ = this.tasksService.approveRDAccount(account.id, payload);
|
error: () => {
|
||||||
} else {
|
settled++;
|
||||||
approve$ = this.tasksService.approveSavingsAccount(account.id, payload);
|
if (settled === selected.length) {
|
||||||
}
|
this.reload();
|
||||||
approve$.subscribe(() => {
|
}
|
||||||
done++;
|
|
||||||
if (done === selected.length) {
|
|
||||||
this.reload();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+7
-63
@@ -68,8 +68,13 @@
|
|||||||
<ng-container matColumnDef="actions">
|
<ng-container matColumnDef="actions">
|
||||||
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
||||||
<td mat-cell *matCellDef="let edit">
|
<td mat-cell *matCellDef="let edit">
|
||||||
<button mat-stroked-button class="psae-btn psae-btn-view" (click)="viewCompare(edit)">
|
<button
|
||||||
<fa-icon icon="eye" class="m-r-10"></fa-icon>Compare
|
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>
|
||||||
<button mat-stroked-button class="psae-btn psae-btn-approve" (click)="approve(edit)">
|
<button mat-stroked-button class="psae-btn psae-btn-approve" (click)="approve(edit)">
|
||||||
<fa-icon icon="check" class="m-r-10"></fa-icon>Approve
|
<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>
|
<tr mat-row *matRowDef="let row; columns: displayedColumns" class="table-row"></tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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) {
|
@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 {
|
.empty-state {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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 { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component';
|
||||||
import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base';
|
import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base';
|
||||||
import { InputBase } from 'app/shared/form-dialog/formfield/model/input-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';
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -79,7 +80,8 @@ export class PendingSavingsAccountEditsComponent {
|
|||||||
'actions'
|
'actions'
|
||||||
];
|
];
|
||||||
|
|
||||||
selectedEdit: any = null;
|
/** Set only while the current account is being fetched, so the button can show progress. */
|
||||||
|
loadingCompareFor: any = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.route.data.subscribe((data: { pendingSavingsAccountEditsData: any; pendingCurrentAccountEditsData: any }) => {
|
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');
|
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 }[] {
|
* Fetches the account's current Digital & Account Feature config so the popup can show it next to
|
||||||
if (!obj) return [];
|
* what's proposed, then opens the compare dialog. The maker's edit form already submits only the
|
||||||
return Object.keys(obj).map((key) => ({ key, value: obj[key] }));
|
* 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 {
|
viewCompare(edit: any): void {
|
||||||
this.selectedEdit = this.selectedEdit === edit ? null : edit;
|
this.loadingCompareFor = edit;
|
||||||
}
|
const isCurrent = edit.accountType === 'Current';
|
||||||
|
const account$ = isCurrent
|
||||||
closeCompare(): void {
|
? this.currentAccountsService.getCurrentAccountAndTemplate(edit.currentAccountId, false)
|
||||||
this.selectedEdit = null;
|
: 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 {
|
approve(edit: any): void {
|
||||||
@@ -189,7 +210,6 @@ export class PendingSavingsAccountEditsComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reload(): void {
|
reload(): void {
|
||||||
this.closeCompare();
|
|
||||||
const url = this.router.url;
|
const url = this.router.url;
|
||||||
this.router
|
this.router
|
||||||
.navigateByUrl('/checker-inbox-and-tasks', { skipLocationChange: true })
|
.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 });
|
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
|
* Reject a savings account
|
||||||
*/
|
*/
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@
|
|||||||
window["env"]["apiActuator"] = '';
|
window["env"]["apiActuator"] = '';
|
||||||
|
|
||||||
window["env"]["fineractPlatformTenantId"] = '';
|
window["env"]["fineractPlatformTenantId"] = '';
|
||||||
window["env"]["fineractPlatformTenantIds"] = '';
|
window["env"]["fineractPlatformTenantIds"] = 'default,socity_bank';
|
||||||
|
|
||||||
window['env']['tenantLogoUrl'] = '';
|
window['env']['tenantLogoUrl'] = '';
|
||||||
window['env']['tenantLogoUrlDark'] = '';
|
window['env']['tenantLogoUrlDark'] = '';
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
export default {
|
export default {
|
||||||
'mifos_x': {
|
'mifos_x': {
|
||||||
'version': '260827',
|
'version': '260827',
|
||||||
'hash': '1273bdbe'
|
'hash': '79ffde51'
|
||||||
},
|
},
|
||||||
'allow_switching_backend_instance': true
|
'allow_switching_backend_instance': true
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user