feat: recurring deposit enhancements, dashboard, chart, and account view updates
This commit is contained in:
+2
-1
@@ -70,8 +70,9 @@ export class ActivateRecurringDepositsAccountComponent implements OnInit {
|
||||
*/
|
||||
createActivateRecurringDepositsAccountForm() {
|
||||
this.activateRecurringDepositsAccountForm = this.formBuilder.group({
|
||||
// Defaults to the business date (the branch's working day), which is what the account is normally activated on.
|
||||
activatedOnDate: [
|
||||
'',
|
||||
this.settingsService.businessDate,
|
||||
Validators.required
|
||||
]
|
||||
});
|
||||
|
||||
+2
-1
@@ -72,8 +72,9 @@ export class ApproveRecurringDepositsAccountComponent implements OnInit {
|
||||
*/
|
||||
createApproveRecurringDepositsAccountForm() {
|
||||
this.approveRecurringDepositsAccountForm = this.formBuilder.group({
|
||||
// Defaults to the business date (the branch's working day), which is what the account is normally approved on.
|
||||
approvedOnDate: [
|
||||
'',
|
||||
this.settingsService.businessDate,
|
||||
Validators.required
|
||||
],
|
||||
note: ['']
|
||||
|
||||
+11
-1
@@ -13,6 +13,16 @@
|
||||
<div class="layout-column">
|
||||
<h3 class="mat-h3 flex-fill">{{ title }}</h3>
|
||||
|
||||
@if (totalLienAmount > 0) {
|
||||
<p class="lien-warn">
|
||||
This deposit is pledged as loan security (lien {{ totalLienAmount | number: '1.2-2' }} -
|
||||
@for (lien of liens; track lien.loanId; let last = $last) {
|
||||
loan {{ lien.loanAccountNo }}{{ last ? '' : ', ' }}
|
||||
}
|
||||
). It cannot be closed until the loan is repaid or the guarantee is released.
|
||||
</p>
|
||||
}
|
||||
|
||||
<mat-form-field (click)="closedOnDatePicker.open()">
|
||||
<mat-label>{{ 'labels.inputs.Closed On' | translate }}</mat-label>
|
||||
<input
|
||||
@@ -154,7 +164,7 @@
|
||||
<button type="button" mat-raised-button [routerLink]="['../general']">
|
||||
{{ 'labels.buttons.Cancel' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button color="primary" [disabled]="!closeRecurringDepositForm.valid">
|
||||
<button mat-raised-button color="primary" [disabled]="!closeRecurringDepositForm.valid || totalLienAmount > 0">
|
||||
{{ 'labels.buttons.Submit' | translate }}
|
||||
</button>
|
||||
</mat-card-actions>
|
||||
|
||||
+6
@@ -10,6 +10,12 @@
|
||||
max-width: 37rem;
|
||||
}
|
||||
|
||||
.lien-warn {
|
||||
margin: 0 0 16px;
|
||||
font-size: 13px;
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.expandcollapsebutton {
|
||||
margin-top: -7px;
|
||||
margin-left: 2%;
|
||||
|
||||
+15
-1
@@ -64,6 +64,9 @@ export class CloseRecurringDepositsAccountComponent implements OnInit {
|
||||
maxDate = new Date();
|
||||
/** Close Recurring Deposit Account form. */
|
||||
closeRecurringDepositForm: UntypedFormGroup;
|
||||
/** Loans this deposit secures. While any remain, the server refuses to close it. */
|
||||
liens: any[] = [];
|
||||
totalLienAmount = 0;
|
||||
|
||||
/**
|
||||
* Retrieves action details template data from `resolve`
|
||||
@@ -79,6 +82,11 @@ export class CloseRecurringDepositsAccountComponent implements OnInit {
|
||||
this.maturityAmount = data.recurringDepositsAccountActionData.maturityAmount;
|
||||
this.onAccountClosureOptions = data.recurringDepositsAccountActionData.onAccountClosureOptions;
|
||||
this.paymentTypes = data.recurringDepositsAccountActionData.paymentTypeOptions;
|
||||
// A matured deposit closes on or after its maturity date - the server refuses anything earlier.
|
||||
const maturityDate = data.recurringDepositsAccountActionData.maturityDate;
|
||||
if (Array.isArray(maturityDate) && maturityDate.length >= 3) {
|
||||
this.minDate = new Date(maturityDate[0], maturityDate[1] - 1, maturityDate[2]);
|
||||
}
|
||||
if (data.recurringDepositsAccountActionData.maturityAmount) {
|
||||
this.title = 'Recurring Deposit Closure on Maturity';
|
||||
} else {
|
||||
@@ -92,6 +100,10 @@ export class CloseRecurringDepositsAccountComponent implements OnInit {
|
||||
this.maxDate = this.settingsService.businessDate;
|
||||
this.createcloseRecurringDepositForm();
|
||||
this.addTransferDetails();
|
||||
this.recurringDepositsService.getRecurringDepositLiens(this.accountId).subscribe((response: any) => {
|
||||
this.liens = response?.liens ?? [];
|
||||
this.totalLienAmount = Number(response?.totalLienAmount) || 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,8 +111,10 @@ export class CloseRecurringDepositsAccountComponent implements OnInit {
|
||||
*/
|
||||
createcloseRecurringDepositForm() {
|
||||
this.closeRecurringDepositForm = this.formBuilder.group({
|
||||
// The business date, not the computer's clock: the branch's working day can differ from the calendar day,
|
||||
// and closing on the clock's date (before maturity, or before the deposit's last installment) fails.
|
||||
closedOnDate: [
|
||||
new Date(),
|
||||
this.settingsService.businessDate,
|
||||
Validators.required
|
||||
],
|
||||
maturityAmount: [{ value: this.maturityAmount, disabled: true }],
|
||||
|
||||
+131
-2
@@ -11,6 +11,51 @@
|
||||
<form [formGroup]="prematureCloseRecurringDepositsAccountForm" (ngSubmit)="submit()">
|
||||
<mat-card-content>
|
||||
<div class="layout-column">
|
||||
@if (account) {
|
||||
<div class="pc-context">
|
||||
<div class="pc-row">
|
||||
<span>Account</span><b>{{ account.accountNo }}</b>
|
||||
</div>
|
||||
<div class="pc-row">
|
||||
<span>Current balance</span><b>{{ currencySymbol }} {{ balance | number: '1.2-2' }}</b>
|
||||
</div>
|
||||
@if (account.maturityDate) {
|
||||
<div class="pc-row">
|
||||
<span>Maturity date</span><b>{{ account.maturityDate | dateFormat }}</b>
|
||||
</div>
|
||||
}
|
||||
@if (account.maturityAmount != null) {
|
||||
<div class="pc-row">
|
||||
<span>Amount at maturity</span><b>{{ currencySymbol }} {{ account.maturityAmount | number: '1.2-2' }}</b>
|
||||
</div>
|
||||
}
|
||||
<div class="pc-row">
|
||||
<span>Pre-closure penalty</span>
|
||||
<b>
|
||||
@if (account.preClosurePenalApplicable) {
|
||||
{{ account.preClosurePenalInterest }}% less interest
|
||||
@if (account.preClosurePenalInterestOnType?.value) {
|
||||
({{ account.preClosurePenalInterestOnType.value }})
|
||||
}
|
||||
} @else {
|
||||
None
|
||||
}
|
||||
</b>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (totalLienAmount > 0) {
|
||||
<p class="pc-warn">
|
||||
This deposit is pledged as loan security (lien {{ currencySymbol }}
|
||||
{{ totalLienAmount | number: '1.2-2' }} -
|
||||
@for (lien of liens; track lien.loanId; let last = $last) {
|
||||
loan {{ lien.loanAccountNo }}{{ last ? '' : ', ' }}
|
||||
}
|
||||
). It cannot be closed until the loan is repaid or the guarantee is released.
|
||||
</p>
|
||||
}
|
||||
|
||||
<mat-form-field (click)="closedOnDatePicker.open()">
|
||||
<mat-label>{{ 'labels.inputs.Closed On' | translate }}</mat-label>
|
||||
<input
|
||||
@@ -23,6 +68,7 @@
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="closedOnDatePicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #closedOnDatePicker></mat-datepicker>
|
||||
<mat-hint>Must be before the maturity date - on or after it, use Close instead.</mat-hint>
|
||||
@if (prematureCloseRecurringDepositsAccountForm.controls.closedOnDate.hasError('required')) {
|
||||
<mat-error>
|
||||
{{ 'labels.inputs.Closed On Date' | translate }} {{ 'labels.commons.is' | translate }}
|
||||
@@ -30,6 +76,89 @@
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
@if (reasonOptions.length > 1) {
|
||||
<mat-form-field subscriptSizing="dynamic">
|
||||
<mat-label>Closure reason</mat-label>
|
||||
<mat-select formControlName="prematureClosureReasonId">
|
||||
@for (reason of reasonOptions; track reason.id) {
|
||||
<mat-option [value]="reason.id">{{ reason.label }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
<mat-hint>{{ selectedReasonHint }}</mat-hint>
|
||||
</mat-form-field>
|
||||
}
|
||||
|
||||
<div class="pc-calc">
|
||||
<button
|
||||
type="button"
|
||||
mat-stroked-button
|
||||
color="primary"
|
||||
(click)="calculate()"
|
||||
[disabled]="
|
||||
calculating || totalLienAmount > 0 || !prematureCloseRecurringDepositsAccountForm.value.closedOnDate
|
||||
"
|
||||
>
|
||||
{{ calculating ? 'Calculating…' : 'Calculate Payout' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (preview) {
|
||||
<div class="pc-result">
|
||||
<div class="pc-row pc-row--total">
|
||||
<span>Payable on premature closure</span>
|
||||
<b>{{ currencySymbol }} {{ preview.payoutAmount | number: '1.2-2' }}</b>
|
||||
</div>
|
||||
@if (shortfallVsMaturity != null && shortfallVsMaturity > 0) {
|
||||
<div class="pc-row pc-row--muted">
|
||||
<span>Less than at maturity by</span>
|
||||
<b>{{ currencySymbol }} {{ shortfallVsMaturity | number: '1.2-2' }}</b>
|
||||
</div>
|
||||
}
|
||||
<p class="pc-note">
|
||||
Paid by transfer to the client's savings account - a recurring deposit cannot be broken for cash.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@if (destinationAccounts.length === 0) {
|
||||
<p class="pc-warn">
|
||||
This client has no active savings account to receive the proceeds. Open one first, then close this
|
||||
deposit.
|
||||
</p>
|
||||
} @else {
|
||||
<mat-form-field>
|
||||
<mat-label>{{ 'labels.inputs.Transfer to Savings' | translate }}</mat-label>
|
||||
<mat-select required formControlName="toSavingsAccountId">
|
||||
@for (savings of destinationAccounts; track savings.id) {
|
||||
<mat-option [value]="savings.id">
|
||||
{{ savings.accountNo }} - {{ savings.productName }}
|
||||
@if (savings.owner) {
|
||||
(nominee: {{ savings.owner }})
|
||||
} @else if (savings.id === account?.linkedAccountId) {
|
||||
(linked)
|
||||
}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
@if (prematureCloseRecurringDepositsAccountForm.controls.toSavingsAccountId.hasError('required')) {
|
||||
<mat-error>
|
||||
{{ 'labels.inputs.Transfer to savings account' | translate }} {{ 'labels.commons.is' | translate }}
|
||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field>
|
||||
<mat-label>{{ 'labels.inputs.Transfer Description' | translate }}</mat-label>
|
||||
<input matInput formControlName="transferDescription" />
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field>
|
||||
<mat-label>{{ 'labels.inputs.Note' | translate }}</mat-label>
|
||||
<textarea matInput cdkTextareaAutosize formControlName="note"></textarea>
|
||||
</mat-form-field>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</mat-card-content>
|
||||
|
||||
@@ -37,8 +166,8 @@
|
||||
<button type="button" mat-raised-button [routerLink]="['../../']">
|
||||
{{ 'labels.buttons.Cancel' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button color="primary" [disabled]="!prematureCloseRecurringDepositsAccountForm">
|
||||
{{ 'labels.buttons.Confirm' | translate }}
|
||||
<button mat-raised-button color="primary" [disabled]="!canConfirm">
|
||||
{{ submitting ? 'Closing…' : ('labels.buttons.Confirm' | translate) }}
|
||||
</button>
|
||||
</mat-card-actions>
|
||||
</form>
|
||||
|
||||
+42
@@ -9,3 +9,45 @@
|
||||
.container {
|
||||
max-width: 37rem;
|
||||
}
|
||||
|
||||
.pc-context,
|
||||
.pc-result {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 12px 14px;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid rgb(128 128 128 / 30%);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.pc-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
font-size: 13px;
|
||||
|
||||
&--total {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
&--muted {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
.pc-calc {
|
||||
margin: 4px 0 16px;
|
||||
}
|
||||
|
||||
.pc-note {
|
||||
margin: 4px 0 0;
|
||||
font-size: 12px;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.pc-warn {
|
||||
margin: 0 0 16px;
|
||||
font-size: 13px;
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
+258
-51
@@ -8,25 +8,41 @@
|
||||
|
||||
/** Angular Imports */
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { CdkTextareaAutosize } from '@angular/cdk/text-field';
|
||||
|
||||
/** Custom Services */
|
||||
import { RecurringDepositsService } from '../../recurring-deposits.service';
|
||||
import { ClientsService } from 'app/clients/clients.service';
|
||||
import { SettingsService } from 'app/settings/settings.service';
|
||||
import { Dates } from 'app/core/utils/dates';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
/** DepositAccountOnClosureType.TRANSFER_TO_SAVINGS - the only way a broken RD may be paid out. */
|
||||
const TRANSFER_TO_SAVINGS = 200;
|
||||
|
||||
/** PrematureClosureReasonType values the RD closure understands. */
|
||||
const REASON_NORMAL = 100;
|
||||
const REASON_DECEASED = 400;
|
||||
const REASON_NOMINEE_HEIR = 500;
|
||||
|
||||
/**
|
||||
* Premature Close Recurring Deposits Account Component
|
||||
*
|
||||
* The server will only break a recurring deposit early by transferring the proceeds to one of the client's savings
|
||||
* accounts (no cash payout, no reinvest), and it needs to be told so: onAccountClosureId and toSavingsAccountId are
|
||||
* mandatory. This screen used to send only the closure date, so every premature closure was rejected. It now asks the
|
||||
* server what the deposit would pay on the chosen date (calculatePrematureAmount, which applies the product's
|
||||
* pre-closure penalty) and only lets the closure be confirmed against that exact figure.
|
||||
*/
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-premature-close-recurring-deposit-account',
|
||||
templateUrl: './premature-close-recurring-deposit-account.component.html',
|
||||
styleUrls: ['./premature-close-recurring-deposit-account.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
CdkTextareaAutosize
|
||||
]
|
||||
})
|
||||
export class PrematureCloseRecurringDepositAccountComponent implements OnInit {
|
||||
@@ -36,72 +52,263 @@ export class PrematureCloseRecurringDepositAccountComponent implements OnInit {
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private settingsService = inject(SettingsService);
|
||||
private clientsService = inject(ClientsService);
|
||||
|
||||
/** Minimum date allowed. */
|
||||
/** Savings accounts of this deposit's nominees - offered as destinations only for a deceased holder. */
|
||||
nomineeSavingsAccounts: any[] = [];
|
||||
|
||||
/** Minimum date allowed: the deposit's activation date once loaded. */
|
||||
minDate = new Date(2000, 0, 1);
|
||||
/** Maximum date allowed. */
|
||||
/** Maximum date allowed: the business date, and never the maturity date or later (that is a normal Close). */
|
||||
maxDate = new Date();
|
||||
/** premature close Recurring Deposits Account form. */
|
||||
prematureCloseRecurringDepositsAccountForm: UntypedFormGroup;
|
||||
/** Recurring Deposits Account Id */
|
||||
accountId: any;
|
||||
/** The deposit being closed, for the context panel and the default destination account. */
|
||||
account: any;
|
||||
|
||||
/** Result of the last calculatePrematureAmount call, and the date it was calculated for. */
|
||||
preview: { payoutAmount: number; savingsAccounts: any[]; closureOptionId: number } | null = null;
|
||||
previewedFor: string | null = null;
|
||||
calculating = false;
|
||||
submitting = false;
|
||||
/** Loans this deposit secures. While any remain, the server refuses to close it. */
|
||||
liens: any[] = [];
|
||||
totalLienAmount = 0;
|
||||
|
||||
/**
|
||||
* @param {FormBuilder} formBuilder Form Builder
|
||||
* @param {RecurringDepositsService} recurringDepositsService Recurring Deposits Service
|
||||
* @param {Dates} dateUtils Date Utils
|
||||
* @param {ActivatedRoute} route Activated Route
|
||||
* @param {Router} router Router
|
||||
* @param {SettingsService} settingsService Settings Service
|
||||
*/
|
||||
constructor() {
|
||||
this.accountId = this.route.parent.snapshot.params['recurringDepositAccountId'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the premature close recurring deposits form.
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.maxDate = this.settingsService.businessDate;
|
||||
this.createprematureCloseRecurringDepositsAccountForm();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the premature close recurring deposits account form.
|
||||
*/
|
||||
createprematureCloseRecurringDepositsAccountForm() {
|
||||
this.prematureCloseRecurringDepositsAccountForm = this.formBuilder.group({
|
||||
closedOnDate: [
|
||||
'',
|
||||
Validators.required
|
||||
]
|
||||
this.recurringDepositsService.getRecurringDepositsAccountData(this.accountId).subscribe((account: any) => {
|
||||
this.account = account;
|
||||
const activatedOn = this.toDate(account?.timeline?.activatedOnDate);
|
||||
if (activatedOn) {
|
||||
this.minDate = activatedOn;
|
||||
}
|
||||
const maturity = this.toDate(account?.maturityDate);
|
||||
if (maturity) {
|
||||
const dayBeforeMaturity = new Date(maturity);
|
||||
dayBeforeMaturity.setDate(dayBeforeMaturity.getDate() - 1);
|
||||
if (dayBeforeMaturity < this.maxDate) {
|
||||
this.maxDate = dayBeforeMaturity;
|
||||
}
|
||||
}
|
||||
});
|
||||
this.recurringDepositsService.getRecurringDepositLiens(this.accountId).subscribe((response: any) => {
|
||||
this.liens = response?.liens ?? [];
|
||||
this.totalLienAmount = Number(response?.totalLienAmount) || 0;
|
||||
});
|
||||
// A different date or reason is a different payout - the old preview no longer describes what would happen.
|
||||
this.prematureCloseRecurringDepositsAccountForm.get('closedOnDate').valueChanges.subscribe(() => {
|
||||
this.preview = null;
|
||||
this.previewedFor = null;
|
||||
});
|
||||
this.prematureCloseRecurringDepositsAccountForm.get('prematureClosureReasonId').valueChanges.subscribe((reason: number) => {
|
||||
this.preview = null;
|
||||
this.previewedFor = null;
|
||||
if (reason === REASON_DECEASED) {
|
||||
this.loadNomineeSavingsAccounts();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the form and premature closes the recurring deposit account,
|
||||
* if successful redirects to the recurring deposit account.
|
||||
*/
|
||||
submit() {
|
||||
const prematureCloseRecurringDepositsAccountFormData = this.prematureCloseRecurringDepositsAccountForm.value;
|
||||
const locale = this.settingsService.language.code;
|
||||
const dateFormat = this.settingsService.dateFormat;
|
||||
const prevClosedOnDate: Date = this.prematureCloseRecurringDepositsAccountForm.value.closedOnDate;
|
||||
if (prematureCloseRecurringDepositsAccountFormData.closedOnDate instanceof Date) {
|
||||
prematureCloseRecurringDepositsAccountFormData.closedOnDate = this.dateUtils.formatDate(
|
||||
prevClosedOnDate,
|
||||
dateFormat
|
||||
);
|
||||
/** Reasons this deposit's product allows; Normal is always there. */
|
||||
get reasonOptions(): Array<{ id: number; label: string; hint: string }> {
|
||||
const options = [{ id: REASON_NORMAL, label: 'Normal', hint: "Pre-closure penalty as per the deposit's terms." }];
|
||||
if (this.account?.noPenaltyNomineeHeirClosure) {
|
||||
options.push({ id: REASON_NOMINEE_HEIR, label: 'Nominee / legal heir claim', hint: 'No pre-closure penalty.' });
|
||||
}
|
||||
const data = {
|
||||
...prematureCloseRecurringDepositsAccountFormData,
|
||||
dateFormat,
|
||||
locale
|
||||
};
|
||||
this.recurringDepositsService
|
||||
.executeRecurringDepositsAccountCommand(this.accountId, 'prematureClose', data)
|
||||
.subscribe(() => {
|
||||
this.router.navigate(['../../'], { relativeTo: this.route });
|
||||
if (this.account?.deceasedSettlement) {
|
||||
options.push({
|
||||
id: REASON_DECEASED,
|
||||
label: 'Account holder deceased',
|
||||
hint: "No penalty, allowed inside lock-in; may be paid to a nominee's savings account."
|
||||
});
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
get selectedReasonHint(): string {
|
||||
const reason = this.prematureCloseRecurringDepositsAccountForm.value.prematureClosureReasonId;
|
||||
return this.reasonOptions.find((option) => option.id === reason)?.hint ?? '';
|
||||
}
|
||||
|
||||
/** The client's own savings accounts, plus - for a deceased holder - the nominees' savings accounts. */
|
||||
get destinationAccounts(): any[] {
|
||||
const own = (this.preview?.savingsAccounts ?? []).map((acc: any) => ({ ...acc, owner: null }));
|
||||
if (this.prematureCloseRecurringDepositsAccountForm.value.prematureClosureReasonId !== REASON_DECEASED) {
|
||||
return own;
|
||||
}
|
||||
const ownIds = new Set(own.map((acc: any) => acc.id));
|
||||
return [
|
||||
...own,
|
||||
...this.nomineeSavingsAccounts.filter((acc: any) => !ownIds.has(acc.id))
|
||||
];
|
||||
}
|
||||
|
||||
private loadNomineeSavingsAccounts() {
|
||||
this.nomineeSavingsAccounts = [];
|
||||
const clientId = this.account?.clientId;
|
||||
if (!clientId || !this.account?.accountNo) {
|
||||
return;
|
||||
}
|
||||
this.clientsService.getClientNominees(clientId, this.account.accountNo, 'RD').subscribe((nominees: any) => {
|
||||
const list: any[] = Array.isArray(nominees) ? nominees : (nominees?.pageItems ?? []);
|
||||
list
|
||||
.filter((nominee: any) => nominee?.nomineeClientId && nominee.isActive !== false)
|
||||
.forEach((nominee: any) => {
|
||||
this.clientsService.getClientAccountData(nominee.nomineeClientId).subscribe((accounts: any) => {
|
||||
const savings = (accounts?.savingsAccounts ?? []).filter(
|
||||
(acc: any) => acc?.depositType?.id === 100 && acc?.status?.active
|
||||
);
|
||||
this.nomineeSavingsAccounts = [
|
||||
...this.nomineeSavingsAccounts,
|
||||
...savings.map((acc: any) => ({
|
||||
id: acc.id,
|
||||
accountNo: acc.accountNo,
|
||||
productName: acc.productName,
|
||||
owner: nominee.nomineeDisplayName
|
||||
}))
|
||||
];
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
createprematureCloseRecurringDepositsAccountForm() {
|
||||
this.prematureCloseRecurringDepositsAccountForm = this.formBuilder.group({
|
||||
closedOnDate: [
|
||||
this.settingsService.businessDate,
|
||||
Validators.required
|
||||
],
|
||||
prematureClosureReasonId: [REASON_NORMAL],
|
||||
toSavingsAccountId: [
|
||||
null,
|
||||
Validators.required
|
||||
],
|
||||
transferDescription: ['Premature closure of recurring deposit'],
|
||||
note: ['']
|
||||
});
|
||||
}
|
||||
|
||||
get balance(): number | null {
|
||||
return this.account?.summary?.accountBalance ?? null;
|
||||
}
|
||||
|
||||
get currencySymbol(): string {
|
||||
return this.account?.currency?.displaySymbol || this.account?.currency?.code || '';
|
||||
}
|
||||
|
||||
/** What breaking early costs compared with letting the deposit run to maturity, when both are known. */
|
||||
get shortfallVsMaturity(): number | null {
|
||||
const maturityAmount = this.account?.maturityAmount;
|
||||
if (!this.preview || maturityAmount == null) {
|
||||
return null;
|
||||
}
|
||||
return Number(maturityAmount) - Number(this.preview.payoutAmount);
|
||||
}
|
||||
|
||||
get canConfirm(): boolean {
|
||||
return (
|
||||
this.totalLienAmount <= 0 &&
|
||||
!!this.preview &&
|
||||
this.previewedFor === this.formattedClosedOnDate &&
|
||||
this.prematureCloseRecurringDepositsAccountForm.valid &&
|
||||
!this.submitting
|
||||
);
|
||||
}
|
||||
|
||||
private get formattedClosedOnDate(): string | null {
|
||||
const value = this.prematureCloseRecurringDepositsAccountForm.value.closedOnDate;
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
return value instanceof Date ? this.dateUtils.formatDate(value, this.settingsService.dateFormat) : value;
|
||||
}
|
||||
|
||||
/** Asks the server what the deposit pays if broken on the chosen date. */
|
||||
calculate() {
|
||||
const closedOnDate = this.formattedClosedOnDate;
|
||||
if (!closedOnDate) {
|
||||
return;
|
||||
}
|
||||
this.calculating = true;
|
||||
const data = {
|
||||
closedOnDate,
|
||||
prematureClosureReasonId: this.prematureCloseRecurringDepositsAccountForm.value.prematureClosureReasonId,
|
||||
dateFormat: this.settingsService.dateFormat,
|
||||
locale: this.settingsService.language.code
|
||||
};
|
||||
this.recurringDepositsService.executeRecurringDepositsAccountCommand(this.accountId, 'calculatePrematureAmount', data).subscribe({
|
||||
next: (response: any) => {
|
||||
const savingsAccounts = response?.savingsAccounts ?? [];
|
||||
const closureOptionId =
|
||||
(response?.onAccountClosureOptions ?? []).find((option: any) => option.id === TRANSFER_TO_SAVINGS)?.id ??
|
||||
TRANSFER_TO_SAVINGS;
|
||||
this.preview = { payoutAmount: response?.maturityAmount, savingsAccounts, closureOptionId };
|
||||
this.previewedFor = closedOnDate;
|
||||
this.calculating = false;
|
||||
this.selectDefaultDestination(savingsAccounts);
|
||||
},
|
||||
error: () => {
|
||||
this.calculating = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** The deposit's own linked savings account, when it has one the client can still receive into. */
|
||||
private selectDefaultDestination(savingsAccounts: any[]) {
|
||||
const control = this.prematureCloseRecurringDepositsAccountForm.get('toSavingsAccountId');
|
||||
if (control.value && savingsAccounts.some((acc: any) => acc.id === control.value)) {
|
||||
return;
|
||||
}
|
||||
const linkedId = this.account?.linkedAccountId;
|
||||
const linked = savingsAccounts.find((acc: any) => acc.id === linkedId);
|
||||
control.setValue(linked?.id ?? (savingsAccounts.length === 1 ? savingsAccounts[0].id : null));
|
||||
}
|
||||
|
||||
submit() {
|
||||
if (!this.canConfirm) {
|
||||
return;
|
||||
}
|
||||
const formValue = this.prematureCloseRecurringDepositsAccountForm.value;
|
||||
const data: any = {
|
||||
closedOnDate: this.formattedClosedOnDate,
|
||||
onAccountClosureId: this.preview.closureOptionId,
|
||||
prematureClosureReasonId: formValue.prematureClosureReasonId,
|
||||
toSavingsAccountId: formValue.toSavingsAccountId,
|
||||
transferDescription: formValue.transferDescription,
|
||||
dateFormat: this.settingsService.dateFormat,
|
||||
locale: this.settingsService.language.code
|
||||
};
|
||||
if (formValue.note) {
|
||||
data.note = formValue.note;
|
||||
}
|
||||
this.submitting = true;
|
||||
this.recurringDepositsService.executeRecurringDepositsAccountCommand(this.accountId, 'prematureClose', data).subscribe({
|
||||
next: () => {
|
||||
this.router.navigate(['../../'], { relativeTo: this.route });
|
||||
},
|
||||
error: () => {
|
||||
this.submitting = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** API dates arrive as [yyyy, m, d] arrays or ISO strings. */
|
||||
private toDate(value: any): Date | null {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return new Date(value[0], value[1] - 1, value[2]);
|
||||
}
|
||||
const parsed = new Date(value);
|
||||
return isNaN(parsed.getTime()) ? null : parsed;
|
||||
}
|
||||
}
|
||||
|
||||
+9
-1
@@ -30,6 +30,14 @@
|
||||
@if (actions['Close']) {
|
||||
<mifosx-close-recurring-deposits-account></mifosx-close-recurring-deposits-account>
|
||||
}
|
||||
@if (actions['Deposit'] || actions['Withdrawal']) {
|
||||
<!-- A direct deposit from the account screen skips the manager's approval; RD installments come in through the
|
||||
cashier's Receipt or Auto Debit instead, so the Deposit action is not offered here even by URL. -->
|
||||
@if (actions['Deposit']) {
|
||||
<p>
|
||||
RD installments cannot be deposited from the account screen. Use a cashier Receipt, or Auto Debit from the linked
|
||||
savings account.
|
||||
</p>
|
||||
}
|
||||
@if (actions['Withdrawal']) {
|
||||
<mifosx-deposit-recurring-deposits-account [currency]="currency"></mifosx-deposit-recurring-deposits-account>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user