diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/premature-close-dialog/premature-close-dialog.component.html b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/premature-close-dialog/premature-close-dialog.component.html new file mode 100644 index 000000000..018b95331 --- /dev/null +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/premature-close-dialog/premature-close-dialog.component.html @@ -0,0 +1,194 @@ + + +
+
+
+ +
+

{{ 'labels.heading.Premature Close' | translate }}

+

+ Close {{ data.accountNo ?? 'this account' }} before maturity -- interest is recalculated as of the date below, + usually at a lower, penalty rate. +

+
+
+ +
+ +
+ @if (loadFailed) { + + } @else { +
+ + {{ 'labels.inputs.Premature Close Date' | translate }} + + + + @if (prematureCloseForm.controls.closedOnDate.hasError('required')) { + + {{ 'labels.inputs.Premature Close Date' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + + + @if (loadingAmount) { +
+ + Calculating premature-closure amount... +
+ } @else { + @if (maturityAmount !== null && maturityAmount !== undefined) { +
+ @if (data.depositAmount) { +
+ Deposit Amount + {{ data.depositAmount | formatNumber }} +
+ } + @if (data.nominalAnnualInterestRate) { +
+ Contracted Interest Rate + {{ data.nominalAnnualInterestRate }}% +
+ } + @if (data.preClosurePenalApplicable) { +
+ Pre-Closure Penalty (on {{ data.preClosurePenalInterestOnType?.value }}) + − {{ data.preClosurePenalInterest }}% +
+ } + @if (interestEarned !== null) { +
+ Interest Earned (to {{ 'labels.inputs.Premature Close Date' | translate }}) + + {{ interestEarned | formatNumber }} +
+ } +
+ {{ 'labels.inputs.Maturity Amount' | translate }} + {{ maturityAmount | formatNumber }} +
+
+ } + + +
+ @for (closureOption of onAccountClosureOptions; track closureOption) { + + } +
+ @if ( + prematureCloseForm.controls.onAccountClosureId?.hasError('required') && + prematureCloseForm.controls.onAccountClosureId?.touched + ) { +
+ {{ 'labels.inputs.Account action' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} +
+ } + + @if (noDestinationAccounts) { + + } + + @if (prematureCloseForm.contains('toSavingsAccountId')) { + + {{ 'labels.inputs.Transfer to Savings' | translate }} + + @for (account of savingsAccountsData; track account) { + + {{ account.accountNo }} + + } + + @if (prematureCloseForm.controls.toSavingsAccountId?.hasError('required')) { + + {{ 'labels.inputs.Transfer to savings account' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + + } + + @if (prematureCloseForm.contains('transferDescription')) { + + {{ 'labels.inputs.Transfer Description' | translate }} + + + } + + @if (prematureCloseForm.contains('note')) { + + {{ 'labels.inputs.Note' | translate }} + + + } + } + + @if (submitError) { + + } +
+ } +
+ +
+ + +
+
diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/premature-close-dialog/premature-close-dialog.component.scss b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/premature-close-dialog/premature-close-dialog.component.scss new file mode 100644 index 000000000..289c1380e --- /dev/null +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/premature-close-dialog/premature-close-dialog.component.scss @@ -0,0 +1,388 @@ +/** + * 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/. + */ + +/** + * Premature Close Dialog Styles + * CBSNexora Core Banking System + * + * Same shape/spacing/typography as the Payment dialog (.pmt__*), in this Danger Zone action's own red + * accent on the header and Confirm button, rather than Payment's blue -- an irreversible, usually + * penalty-bearing action reads differently from a routine one, everywhere else stays identical so the + * two dialogs feel like the same product. + */ + +.pcd { + display: flex; + flex-direction: column; + height: 100%; + max-height: 92vh; + box-sizing: border-box; + font-family: + 'DM Sans', + system-ui, + -apple-system, + sans-serif; + color: #0f172a; + border-radius: 16px; + overflow: hidden; + box-shadow: 0 24px 48px -12px rgb(15 23 42 / 25%); +} + +.pcd__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 26px 30px; + background: linear-gradient(135deg, #7f1d1d 0%, #b91c1c 55%, #dc2626 100%); + color: #fff; + flex-shrink: 0; + position: relative; + overflow: hidden; + + &::after { + content: ''; + position: absolute; + inset: 0; + background: radial-gradient(circle at 88% -20%, rgb(255 255 255 / 22%), transparent 55%); + pointer-events: none; + } + + &-left { + display: flex; + align-items: center; + gap: 16px; + position: relative; + z-index: 1; + } + + &-ico { + width: 48px; + height: 48px; + border-radius: 14px; + background: rgb(255 255 255 / 16%); + border: 1px solid rgb(255 255 255 / 25%); + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + color: #fff; + flex-shrink: 0; + } + + &-text { + display: flex; + flex-direction: column; + } + + &-close { + position: relative; + z-index: 1; + background: rgb(255 255 255 / 14%); + border: none; + color: #fff; + width: 34px; + height: 34px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + flex-shrink: 0; + + &:hover:not(:disabled) { + background: rgb(255 255 255 / 28%); + transform: rotate(90deg); + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + } +} + +.pcd__title { + margin: 0; + font-size: 20px; + font-weight: 800; + color: #fff; + line-height: 1.25; + letter-spacing: -0.01em; +} + +.pcd__subtitle { + margin: 5px 0 0; + font-size: 13px; + color: rgb(255 255 255 / 88%); + line-height: 1.4; +} + +.pcd__content { + padding: 26px 30px !important; + overflow-y: auto; + flex: 1 1 auto; + background: #fbfcfe; + + mat-form-field { + width: 100%; + } +} + +.pcd__loading { + display: flex; + align-items: center; + gap: 12px; + color: #64748b; + font-size: 13px; + padding: 14px 0 18px; +} + +.pcd__preview { + display: flex; + flex-direction: column; + background: #fff; + border: 1px solid #e2e8f0; + border-radius: 14px; + padding: 4px 18px; + margin: 0 0 18px; + + &-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 11px 0; + border-bottom: 1px solid #f1f5f9; + + &:last-child { + border-bottom: none; + } + + &--highlight { + .pcd__preview-value { + color: #b91c1c; + font-weight: 800; + } + } + + &--total { + margin: 0 -18px; + padding: 14px 18px; + background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%); + border-bottom: none; + border-radius: 0 0 12px 12px; + + .pcd__preview-label { + color: #b91c1c; + font-weight: 700; + } + + .pcd__preview-value { + color: #0f172a; + font-size: 18px; + font-weight: 800; + } + } + } + + &-label { + font-size: 12.5px; + color: #64748b; + font-weight: 600; + } + + &-value { + font-family: 'JetBrains Mono', 'DM Sans', monospace; + font-size: 14px; + font-weight: 700; + color: #0f172a; + } +} + +.pcd__section-label { + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + color: #475569; + margin: 10px 0 12px; +} + +/* Action Options Grid - 2 Equal Columns */ +.pcd__options { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 14px; + margin-bottom: 18px; + + @media (width <= 560px) { + grid-template-columns: 1fr; + } +} + +.pcd__option { + display: flex; + align-items: flex-start; + gap: 14px; + text-align: left; + background: #fff; + border: 1.5px solid #e2e8f0; + border-radius: 14px; + padding: 16px; + cursor: pointer; + font: inherit; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + + &:hover { + border-color: #fca5a5; + background: #f8fafc; + box-shadow: 0 6px 16px rgb(15 23 42 / 6%); + transform: translateY(-1px); + } + + &--active { + border-color: #dc2626; + background: #fef2f2; + box-shadow: 0 0 0 1.5px #dc2626; + + .pcd__option-ico { + background: #dc2626; + color: #fff; + border-color: #dc2626; + } + } + + &-ico { + width: 38px; + height: 38px; + min-width: 38px; + border-radius: 10px; + background: #f1f5f9; + border: 1px solid #e2e8f0; + color: #dc2626; + display: flex; + align-items: center; + justify-content: center; + font-size: 15px; + flex-shrink: 0; + transition: all 0.2s ease; + } + + &-text { + display: flex; + flex-direction: column; + gap: 4px; + flex: 1; + } + + &-title { + font-size: 14px; + font-weight: 700; + color: #0f172a; + line-height: 1.3; + } + + &-desc { + font-size: 12px; + color: #64748b; + line-height: 1.4; + } + + &-check { + color: #dc2626; + font-size: 17px; + position: absolute; + top: 14px; + right: 14px; + } +} + +.pcd__option-error { + color: #b91c1c; + font-size: 12px; + font-weight: 600; + margin: -10px 0 14px; +} + +.pcd__hint { + display: flex; + align-items: flex-start; + gap: 10px; + background: #f0f9ff; + border: 1px solid #bae6fd; + border-left: 4px solid #0284c7; + border-radius: 10px; + color: #0369a1; + font-size: 13px; + line-height: 1.5; + padding: 12px 16px; + margin: 0 0 18px; + + &--warn { + background: #fffbeb; + border-color: #fde68a; + border-left-color: #d97706; + color: #b45309; + } +} + +.pcd__error { + display: flex; + align-items: flex-start; + gap: 10px; + background: #fef2f2; + border: 1px solid #fecaca; + border-left: 4px solid #ef4444; + border-radius: 10px; + color: #b91c1c; + font-size: 13px; + padding: 12px 16px; + margin: 10px 0; +} + +.pcd__actions { + padding: 18px 30px !important; + border-top: 1px solid #e2e8f0; + background: #fff; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 14px; + flex-shrink: 0; + + .pcd__btn-cancel { + border-radius: 10px; + font-weight: 600; + color: #475569; + } + + .pcd__btn-submit { + border-radius: 10px; + font-weight: 700; + padding: 0 26px; + height: 42px; + background: linear-gradient(135deg, #b91c1c 0%, #dc2626 100%); + color: #fff; + box-shadow: 0 8px 20px rgb(220 38 38 / 30%); + transition: all 0.2s ease; + + &:hover:not(:disabled) { + box-shadow: 0 10px 24px rgb(220 38 38 / 40%); + transform: translateY(-1px); + } + + &:disabled { + background: #cbd5e1; + box-shadow: none; + color: #64748b; + } + } +} diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/premature-close-dialog/premature-close-dialog.component.ts b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/premature-close-dialog/premature-close-dialog.component.ts new file mode 100644 index 000000000..2122653e5 --- /dev/null +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/premature-close-dialog/premature-close-dialog.component.ts @@ -0,0 +1,218 @@ +/** + * 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, + MatDialogContent, + MatDialogActions, + MatDialogClose +} from '@angular/material/dialog'; +import { CdkScrollable } from '@angular/cdk/scrolling'; +import { CdkTextareaAutosize } from '@angular/cdk/text-field'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { UntypedFormBuilder, UntypedFormGroup, UntypedFormControl, Validators } from '@angular/forms'; + +/** Custom Services */ +import { FixedDepositsService } from '../../../fixed-deposits.service'; +import { SettingsService } from 'app/settings/settings.service'; +import { Dates } from 'app/core/utils/dates'; +import { FormatNumberPipe } from '../../../../../pipes/format-number.pipe'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * "Premature Close" dialog -- Active-only, no reinvest option (the deposit hasn't matured, so there's + * nothing to renew): pay out in cash, or transfer the pre-maturity balance to a savings account. Same + * `.pcd__*` visual language as the Payment dialog ({@link PaymentDialogComponent}), in this Danger Zone + * action's own red accent rather than Payment's blue, since it's an irreversible, usually + * penalty-bearing action rather than a routine one. + *

+ * The premature amount is date-dependent (unlike Payment's one-shot template load): every change to + * Closed On Date re-calls `calculatePrematureAmount`, same as the full-page form this replaces did. + */ +@Component({ + selector: 'mifosx-premature-close-dialog', + templateUrl: './premature-close-dialog.component.html', + styleUrls: ['./premature-close-dialog.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + CdkScrollable, + MatDialogContent, + MatDialogActions, + MatDialogClose, + CdkTextareaAutosize, + FaIconComponent, + FormatNumberPipe + ] +}) +export class PrematureCloseDialogComponent implements OnInit { + /** The `onAccountClosureId` codes Premature Close can offer -- same codes Payment/Close use. */ + private static readonly WITHDRAW_DEPOSIT_ID = 100; + private static readonly TRANSFER_TO_SAVINGS_ID = 200; + + private static readonly ACTION_ICONS: Record = { + [PrematureCloseDialogComponent.WITHDRAW_DEPOSIT_ID]: 'money-bill-wave', + [PrematureCloseDialogComponent.TRANSFER_TO_SAVINGS_ID]: 'wallet' + }; + + private static readonly ACTION_DESCRIPTIONS: Record = { + [PrematureCloseDialogComponent.WITHDRAW_DEPOSIT_ID]: 'Pay out the full balance in cash', + [PrematureCloseDialogComponent.TRANSFER_TO_SAVINGS_ID]: 'Move the full balance to another savings account' + }; + + dialogRef = inject>(MatDialogRef); + data: { + accountId: number | string; + accountNo?: string; + depositAmount?: number; + nominalAnnualInterestRate?: number; + preClosurePenalApplicable?: boolean; + preClosurePenalInterest?: number; + preClosurePenalInterestOnType?: { value: string }; + } = inject(MAT_DIALOG_DATA); + private formBuilder = inject(UntypedFormBuilder); + private fixedDepositsService = inject(FixedDepositsService); + private settingsService = inject(SettingsService); + private dateUtils = inject(Dates); + + prematureCloseForm: UntypedFormGroup; + onAccountClosureOptions: any[] = []; + savingsAccountsData: any[] = []; + maturityAmount: number = null; + loadingAmount = true; + loadFailed = false; + submitting = false; + submitError: string = null; + minDate = new Date(2000, 0, 1); + maxDate: Date; + + ngOnInit(): void { + this.maxDate = this.settingsService.businessDate; + this.prematureCloseForm = this.formBuilder.group({ + closedOnDate: [ + this.maxDate, + Validators.required + ] + }); + + this.prematureCloseForm + .get('closedOnDate') + .valueChanges.subscribe((date: Date) => this.calculatePrematureAmount(date)); + // Same date the form already starts with -- without this the amount/options never load unless the + // maker happens to touch the date picker, which the full-page form this replaces required too. + this.calculatePrematureAmount(this.maxDate); + } + + private calculatePrematureAmount(date: Date): void { + this.loadingAmount = true; + this.loadFailed = false; + const locale = this.settingsService.language.code; + const dateFormat = this.settingsService.dateFormat; + const data = { + closedOnDate: this.dateUtils.formatDate(date, dateFormat), + dateFormat, + locale + }; + this.fixedDepositsService + .executeFixedDepositsAccountCommand(this.data.accountId as string, 'calculatePrematureAmount', data) + .subscribe({ + next: (response: any) => { + this.savingsAccountsData = response.savingsAccounts ?? []; + this.onAccountClosureOptions = response.onAccountClosureOptions ?? []; + this.maturityAmount = response.maturityAmount; + this.loadingAmount = false; + if (!this.prematureCloseForm.contains('onAccountClosureId')) { + this.prematureCloseForm.addControl('onAccountClosureId', new UntypedFormControl('', Validators.required)); + this.prematureCloseForm.addControl('note', new UntypedFormControl('')); + this.prematureCloseForm + .get('onAccountClosureId') + .valueChanges.subscribe((id: number) => this.onClosureTypeChange(id)); + } + }, + // Without this the spinner would run forever on a failed load, leaving Cancel as the only way out. + error: () => { + this.loadingAmount = false; + this.loadFailed = true; + } + }); + } + + /** Adds/removes the destination-account control to match what the chosen type needs. */ + private onClosureTypeChange(id: number): void { + if (id === PrematureCloseDialogComponent.TRANSFER_TO_SAVINGS_ID) { + if (!this.prematureCloseForm.contains('toSavingsAccountId')) { + this.prematureCloseForm.addControl('toSavingsAccountId', new UntypedFormControl('', Validators.required)); + this.prematureCloseForm.addControl('transferDescription', new UntypedFormControl('')); + } + } else { + this.prematureCloseForm.removeControl('toSavingsAccountId'); + this.prematureCloseForm.removeControl('transferDescription'); + } + } + + /** The gap between the deposit and the computed maturity amount -- the actual interest this premature + * closure earns, at whatever rate (contracted, or the lower pre-closure-penalty one below) applied. */ + get interestEarned(): number | null { + if (this.maturityAmount === null || this.maturityAmount === undefined || !this.data.depositAmount) { + return null; + } + return Number(this.maturityAmount) - Number(this.data.depositAmount); + } + + /** True when the chosen action needs a destination account but the customer has none to pick. */ + get noDestinationAccounts(): boolean { + return ( + !this.loadingAmount && + this.prematureCloseForm?.contains('toSavingsAccountId') && + this.savingsAccountsData.length === 0 + ); + } + + iconFor(id: number): string { + return PrematureCloseDialogComponent.ACTION_ICONS[id] ?? 'circle'; + } + + descriptionFor(id: number): string { + return PrematureCloseDialogComponent.ACTION_DESCRIPTIONS[id] ?? ''; + } + + selectClosureType(id: number): void { + this.prematureCloseForm.get('onAccountClosureId').setValue(id); + this.prematureCloseForm.get('onAccountClosureId').markAsTouched(); + } + + submit(): void { + const formValue = { ...this.prematureCloseForm.value }; + const locale = this.settingsService.language.code; + const dateFormat = this.settingsService.dateFormat; + if (formValue.closedOnDate instanceof Date) { + formValue.closedOnDate = this.dateUtils.formatDate(formValue.closedOnDate, dateFormat); + } + const data = { ...formValue, dateFormat, locale }; + this.submitting = true; + this.submitError = null; + this.fixedDepositsService + .executeFixedDepositsAccountCommand(this.data.accountId as string, 'prematureClose', data) + .subscribe({ + next: () => { + this.dialogRef.close({ confirm: true }); + }, + // Keep the dialog open with the entered values and say what went wrong, rather than leaving a + // permanently disabled Confirm behind whatever modal the global interceptor threw up. + error: (error: any) => { + this.submitting = false; + this.submitError = + error?.error?.defaultUserMessage ?? + error?.error?.errors?.[0]?.defaultUserMessage ?? + 'This account could not be prematurely closed. Please review the details and try again.'; + } + }); + } +} diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposit-account-view.component.ts b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposit-account-view.component.ts index 42f39f6c5..a07dde825 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposit-account-view.component.ts +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposit-account-view.component.ts @@ -17,6 +17,7 @@ import { CalculateInterestDialogComponent } from './custom-dialogs/calculate-int import { PostInterestDialogComponent } from './custom-dialogs/post-interest-dialog/post-interest-dialog.component'; import { ToggleWithholdTaxDialogComponent } from './custom-dialogs/toggle-withhold-tax-dialog/toggle-withhold-tax-dialog.component'; import { PaymentDialogComponent } from './custom-dialogs/payment-dialog/payment-dialog.component'; +import { PrematureCloseDialogComponent } from './custom-dialogs/premature-close-dialog/premature-close-dialog.component'; import { DepositCertificateDialogComponent } from 'app/shared/deposit-certificate-dialog/deposit-certificate-dialog.component'; /** Custom Button Config. */ @@ -238,7 +239,6 @@ export class FixedDepositAccountViewComponent implements OnInit { case 'Undo Activation': case 'Add Charge': case 'Withdrawn by Client': - case 'Premature Close': case 'Deposit': case 'Withdrawal': this.router.navigate([`actions/${name}`], { relativeTo: this.route }); @@ -267,12 +267,17 @@ export class FixedDepositAccountViewComponent implements OnInit { case 'Payment': this.openPaymentDialog(); break; + case 'Premature Close': + this.openPrematureCloseDialog(); + break; } } /** - * Opens the Payment action -- unlike Premature Close/Close, works whether the account is Active or - * Matured, and is the only place a Partial Reinvest can be done. + * Opens the Payment action -- only offered once the FD has actually Matured (see + * FixedDepositsButtonsConfiguration), and the only place a Partial Reinvest can be done. Also the + * exact same dialog the FD Dashboard's Renew action opens (see fd-dashboard.component.ts's + * openRenewDialog), just without dashboardRenewal data, so its date picker stays visible. */ private openPaymentDialog(): void { const paymentDialogRef = this.dialog.open(PaymentDialogComponent, { @@ -293,6 +298,34 @@ export class FixedDepositAccountViewComponent implements OnInit { }); } + /** + * Opens the Premature Close action -- Active-only, no reinvest option, since the deposit hasn't + * matured yet. + */ + private openPrematureCloseDialog(): void { + const prematureCloseDialogRef = this.dialog.open(PrematureCloseDialogComponent, { + width: '720px', + maxWidth: '95vw', + data: { + accountId: this.fixedDepositsAccountData.id, + accountNo: this.fixedDepositsAccountData.fdAccountNumber || this.fixedDepositsAccountData.accountNo, + // calculatePrematureAmount only ever returns the final maturityAmount, not how it was reached -- + // these come from the account's own data (already loaded for this page) so the dialog can show + // the breakdown (deposit, contracted rate, penalty, interest earned) behind that one number. + depositAmount: this.fixedDepositsAccountData.depositAmount, + nominalAnnualInterestRate: this.fixedDepositsAccountData.nominalAnnualInterestRate, + preClosurePenalApplicable: this.fixedDepositsAccountData.preClosurePenalApplicable, + preClosurePenalInterest: this.fixedDepositsAccountData.preClosurePenalInterest, + preClosurePenalInterestOnType: this.fixedDepositsAccountData.preClosurePenalInterestOnType + } + }); + prematureCloseDialogRef.afterClosed().subscribe((response: any) => { + if (response?.confirm) { + this.reload(); + } + }); + } + /** * Deletes Fixed Deposits Account. */ diff --git a/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-account-actions.component.html b/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-account-actions.component.html index f20e46af8..92b1d954b 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-account-actions.component.html +++ b/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-account-actions.component.html @@ -24,9 +24,6 @@ @if (actions['Add Charge']) { } -@if (actions['Premature Close']) { - -} @if (actions['Close']) { } diff --git a/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-account-actions.component.ts b/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-account-actions.component.ts index e0b47a2f7..b587839e8 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-account-actions.component.ts +++ b/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-account-actions.component.ts @@ -15,7 +15,6 @@ import { ActivateFixedDepositsAccountComponent } from './activate-fixed-deposits import { UndoApprovalFixedDepositsAccountComponent } from './undo-approval-fixed-deposits-account/undo-approval-fixed-deposits-account.component'; import { WithdrawByClientFixedDepositsAccountComponent } from './withdraw-by-client-fixed-deposits-account/withdraw-by-client-fixed-deposits-account.component'; import { AddChargeFixedDepositsAccountComponent } from './add-charge-fixed-deposits-account/add-charge-fixed-deposits-account.component'; -import { PrematureCloseFixedDepositsAccountComponent } from './premature-close-fixed-deposits-account/premature-close-fixed-deposits-account.component'; import { CloseFixedDepositsAccountComponent } from './close-fixed-deposits-account/close-fixed-deposits-account.component'; import { FixedDepositsCashTransactionComponent } from './fixed-deposits-cash-transaction/fixed-deposits-cash-transaction.component'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @@ -35,7 +34,6 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; UndoApprovalFixedDepositsAccountComponent, WithdrawByClientFixedDepositsAccountComponent, AddChargeFixedDepositsAccountComponent, - PrematureCloseFixedDepositsAccountComponent, CloseFixedDepositsAccountComponent, FixedDepositsCashTransactionComponent ] @@ -52,7 +50,6 @@ export class FixedDepositsAccountActionsComponent { 'Undo Approval': boolean; 'Undo Activation': boolean; 'Add Charge': boolean; - 'Premature Close': boolean; 'Withdrawn by Client': boolean; Withdrawal: boolean; Deposit: boolean; @@ -64,7 +61,6 @@ export class FixedDepositsAccountActionsComponent { 'Undo Approval': false, 'Undo Activation': false, 'Add Charge': false, - 'Premature Close': false, 'Withdrawn by Client': false, Withdrawal: false, Deposit: false diff --git a/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/premature-close-fixed-deposits-account/premature-close-fixed-deposits-account.component.html b/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/premature-close-fixed-deposits-account/premature-close-fixed-deposits-account.component.html deleted file mode 100644 index 0d204538d..000000000 --- a/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/premature-close-fixed-deposits-account/premature-close-fixed-deposits-account.component.html +++ /dev/null @@ -1,105 +0,0 @@ - - -

- -
- -
- - {{ 'labels.inputs.Premature Close Date' | translate }} - - - - @if (prematureCloseAccountForm.controls.closedOnDate.hasError('required')) { - - {{ 'labels.inputs.Premature Close Date' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - } - - - @if (prematureCloseAccountForm.controls.maturityAmount) { - - {{ 'labels.inputs.Maturity Amount' | translate }} - - - } - - @if (prematureCloseAccountForm.contains('onAccountClosureId')) { - - {{ 'labels.inputs.Action' | translate }} - - @for (closureOption of onAccountClosureOptions; track closureOption) { - - {{ closureOption.value }} - - } - - @if (prematureCloseAccountForm.controls.onAccountClosureId.hasError('required')) { - - {{ 'labels.inputs.Account action' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - } - - } - - @if (prematureCloseAccountForm.contains('toSavingsAccountId')) { - - {{ 'labels.inputs.Transfer to Savings' | translate }} - - @for (account of savingsAccountsData; track account) { - - {{ account.accountNo }} - - } - - @if (prematureCloseAccountForm.controls.toSavingsAccountId.hasError('required')) { - - {{ 'labels.inputs.Transfer to savings account' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - } - - } - - @if (prematureCloseAccountForm.contains('transferDescription')) { - - {{ 'labels.inputs.Transfer Description' | translate }} - - - } - - @if (prematureCloseAccountForm.contains('note')) { - - {{ 'labels.inputs.Note' | translate }} - - - } -
-
- - - - - -
-
-
diff --git a/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/premature-close-fixed-deposits-account/premature-close-fixed-deposits-account.component.scss b/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/premature-close-fixed-deposits-account/premature-close-fixed-deposits-account.component.scss deleted file mode 100644 index 7c3f1555b..000000000 --- a/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/premature-close-fixed-deposits-account/premature-close-fixed-deposits-account.component.scss +++ /dev/null @@ -1,11 +0,0 @@ -/** - * 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/. - */ - -.container { - max-width: 37rem; -} diff --git a/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/premature-close-fixed-deposits-account/premature-close-fixed-deposits-account.component.ts b/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/premature-close-fixed-deposits-account/premature-close-fixed-deposits-account.component.ts deleted file mode 100644 index 5e4d5dc51..000000000 --- a/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/premature-close-fixed-deposits-account/premature-close-fixed-deposits-account.component.ts +++ /dev/null @@ -1,179 +0,0 @@ -/** - * 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 { - UntypedFormGroup, - UntypedFormBuilder, - Validators, - UntypedFormControl, - ReactiveFormsModule -} from '@angular/forms'; -import { ActivatedRoute, Router, RouterLink } from '@angular/router'; - -/** Custom Services */ -import { FixedDepositsService } from '../../fixed-deposits.service'; -import { SettingsService } from 'app/settings/settings.service'; -import { Dates } from 'app/core/utils/dates'; -import { CdkTextareaAutosize } from '@angular/cdk/text-field'; -import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; - -/** - * Premature Close Fixed Deposits Account Component - */ -@Component({ - selector: 'mifosx-premature-close-fixed-deposits-account', - templateUrl: './premature-close-fixed-deposits-account.component.html', - styleUrls: ['./premature-close-fixed-deposits-account.component.scss'], - imports: [ - ...STANDALONE_SHARED_IMPORTS, - CdkTextareaAutosize - ] -}) -export class PrematureCloseFixedDepositsAccountComponent implements OnInit { - private formBuilder = inject(UntypedFormBuilder); - private fixedDepositsService = inject(FixedDepositsService); - private dateUtils = inject(Dates); - private route = inject(ActivatedRoute); - private router = inject(Router); - private settingsService = inject(SettingsService); - - /** Minimum date allowed. */ - minDate = new Date(2000, 0, 1); - /** Maximum date allowed. */ - maxDate = new Date(); - /** Premature Close FD Account form. */ - prematureCloseAccountForm: UntypedFormGroup; - /** Savings Account Data */ - savingsAccountsData: any; - /** On account Closure Options */ - onAccountClosureOptions: any; - /** Fixed Deposits Account Id */ - accountId: any; - /** Form submission event */ - isSubmitted = false; - - /** - * @param {FormBuilder} formBuilder Form Builder - * @param {FixedDepositsService} fixedDepositsService Fixed 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['fixedDepositAccountId']; - } - - /** - * Creates the premature close fd account form. - */ - ngOnInit() { - this.maxDate = this.settingsService.businessDate; - this.createPrematureCloseAccountForm(); - this.buildDependencies(); - } - - /** - * Creates the premature close fd account form. - */ - createPrematureCloseAccountForm() { - this.prematureCloseAccountForm = this.formBuilder.group({ - closedOnDate: [ - '', - Validators.required - ] - }); - } - - /** - * Subscribes to value changes of parent control. - */ - buildDependencies() { - this.prematureCloseAccountForm.get('closedOnDate').valueChanges.subscribe((value: Date) => { - if (!this.isSubmitted) { - this.calculatePrematureAmount(value); - } - }); - } - - /** - * Calculates prematue amount based on closed on date. - * @param {Date} date Premature Close Date - */ - calculatePrematureAmount(date: Date) { - const locale = this.settingsService.language.code; - const dateFormat = this.settingsService.dateFormat; - const data = { - closedOnDate: this.dateUtils.formatDate(date, dateFormat), - dateFormat, - locale - }; - this.fixedDepositsService - .executeFixedDepositsAccountCommand(this.accountId, 'calculatePrematureAmount', data) - .subscribe((response: any) => { - this.savingsAccountsData = response.savingsAccounts; - this.onAccountClosureOptions = response.onAccountClosureOptions; - this.prematureCloseAccountForm.addControl( - 'maturityAmount', - new UntypedFormControl({ value: '', disabled: true }) - ); - this.prematureCloseAccountForm.addControl( - 'onAccountClosureId', - new UntypedFormControl('', Validators.required) - ); - this.prematureCloseAccountForm.addControl('note', new UntypedFormControl('')); - this.prematureCloseAccountForm.get('maturityAmount').patchValue(response.maturityAmount); - this.addTransferDetails(); - }); - } - - /** - * Subscribes to value changes of `onAccountClosureId` adds and removes transfer details accordingly. - */ - addTransferDetails() { - this.prematureCloseAccountForm.get('onAccountClosureId').valueChanges.subscribe((id: any) => { - if (id === 200) { - this.prematureCloseAccountForm.addControl( - 'toSavingsAccountId', - new UntypedFormControl('', Validators.required) - ); - this.prematureCloseAccountForm.addControl('transferDescription', new UntypedFormControl('')); - } else { - this.prematureCloseAccountForm.removeControl('toSavingsAccountId'); - this.prematureCloseAccountForm.removeControl('transferDescription'); - } - }); - } - - /** - * Submits the form and premature closes the fd account, - * if successful redirects to the fd account. - */ - submit() { - const prematureCloseAccountFormData = this.prematureCloseAccountForm.value; - this.isSubmitted = true; - const locale = this.settingsService.language.code; - const dateFormat = this.settingsService.dateFormat; - const prevClosedDate: Date = this.prematureCloseAccountForm.value.closedOnDate; - if (prematureCloseAccountFormData.closedOnDate instanceof Date) { - prematureCloseAccountFormData.closedOnDate = this.dateUtils.formatDate(prevClosedDate, dateFormat); - } - const data = { - ...prematureCloseAccountFormData, - dateFormat, - locale - }; - this.fixedDepositsService - .executeFixedDepositsAccountCommand(this.accountId, 'prematureClose', data) - .subscribe(() => { - this.router.navigate(['../../'], { relativeTo: this.route }); - }); - } -} diff --git a/src/app/deposits/fixed-deposits/fixed-deposits.module.ts b/src/app/deposits/fixed-deposits/fixed-deposits.module.ts index 6529b1b04..9aa9db4ba 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposits.module.ts +++ b/src/app/deposits/fixed-deposits/fixed-deposits.module.ts @@ -35,7 +35,6 @@ import { ViewTransactionComponent } from './fixed-deposit-account-view/view-tran import { WaiveChargeDialogComponent } from './fixed-deposit-account-view/custom-dialogs/waive-charge-dialog/waive-charge-dialog.component'; import { InactivateChargeDialogComponent } from './fixed-deposit-account-view/custom-dialogs/inactivate-charge-dialog/inactivate-charge-dialog.component'; import { AddChargeFixedDepositsAccountComponent } from './fixed-deposits-account-actions/add-charge-fixed-deposits-account/add-charge-fixed-deposits-account.component'; -import { PrematureCloseFixedDepositsAccountComponent } from './fixed-deposits-account-actions/premature-close-fixed-deposits-account/premature-close-fixed-deposits-account.component'; import { CloseFixedDepositsAccountComponent } from './fixed-deposits-account-actions/close-fixed-deposits-account/close-fixed-deposits-account.component'; import { CreateFixedDepositAccountComponent } from './create-fixed-deposit-account/create-fixed-deposit-account.component'; import { FixedDepositAccountDetailsStepComponent } from './fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component'; @@ -78,7 +77,6 @@ import { FixedDepositsCashTransactionComponent } from './fixed-deposits-account- WaiveChargeDialogComponent, InactivateChargeDialogComponent, AddChargeFixedDepositsAccountComponent, - PrematureCloseFixedDepositsAccountComponent, CloseFixedDepositsAccountComponent, CreateFixedDepositAccountComponent, FixedDepositAccountDetailsStepComponent,