From e58fd3c79a82add03b6614d07b598506c0857008 Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Thu, 20 Aug 2026 11:05:25 +0530 Subject: [PATCH] feat(deposits): sync linked account with maturity terms, add deposit certificate printing Fixed and Recurring Deposit wizards now bidirectionally sync the Product tab's linked-account selection with the new Terms tab's Transfer-to-Savings account, disable the Withdraw Deposit maturity instruction end-to-end, and card-style the Transfer to Savings picker to match the existing linked-account UI. Adds a printable FD/RD deposit certificate from the account view page, following the existing letter-mgmt certificate pattern. Co-Authored-By: Claude Sonnet 5 --- ...reate-fixed-deposit-account.component.html | 37 +- .../create-fixed-deposit-account.component.ts | 11 +- .../edit-fixed-deposit-account.component.html | 37 +- .../edit-fixed-deposit-account.component.ts | 9 +- ...-deposit-account-details-step.component.ts | 34 +- ...account-maturity-terms-step.component.html | 108 ++++ ...account-maturity-terms-step.component.scss | 152 ++++++ ...t-account-maturity-terms-step.component.ts | 117 ++++ ...eposit-account-preview-step.component.html | 35 ++ ...-deposit-account-preview-step.component.ts | 21 +- .../fixed-deposit-account-view.component.html | 10 + .../fixed-deposit-account-view.component.ts | 15 + ...-recurring-deposits-account.component.html | 37 +- ...te-recurring-deposits-account.component.ts | 11 +- ...t-recurring-deposit-account.component.html | 37 +- ...dit-recurring-deposit-account.component.ts | 11 +- ...deposits-account-details-step.component.ts | 35 +- ...account-maturity-terms-step.component.html | 108 ++++ ...account-maturity-terms-step.component.scss | 152 ++++++ ...s-account-maturity-terms-step.component.ts | 116 ++++ ...posits-account-preview-step.component.html | 35 ++ ...deposits-account-preview-step.component.ts | 24 +- ...eposits-account-settings-step.component.ts | 19 + ...rring-deposits-account-view.component.html | 13 + ...curring-deposits-account-view.component.ts | 15 + ...posit-product-settings-step.component.html | 27 + ...posit-product-settings-step.component.scss | 9 + ...deposit-product-settings-step.component.ts | 11 + ...posit-product-settings-step.component.html | 27 + ...posit-product-settings-step.component.scss | 9 + ...deposit-product-settings-step.component.ts | 8 + .../deposit-certificate-dialog.component.html | 160 ++++++ .../deposit-certificate-dialog.component.scss | 512 ++++++++++++++++++ .../deposit-certificate-dialog.component.ts | 132 +++++ 34 files changed, 2079 insertions(+), 15 deletions(-) create mode 100644 src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-maturity-terms-step/fixed-deposit-account-maturity-terms-step.component.html create mode 100644 src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-maturity-terms-step/fixed-deposit-account-maturity-terms-step.component.scss create mode 100644 src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-maturity-terms-step/fixed-deposit-account-maturity-terms-step.component.ts create mode 100644 src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-maturity-terms-step/recurring-deposits-account-maturity-terms-step.component.html create mode 100644 src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-maturity-terms-step/recurring-deposits-account-maturity-terms-step.component.scss create mode 100644 src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-maturity-terms-step/recurring-deposits-account-maturity-terms-step.component.ts create mode 100644 src/app/shared/deposit-certificate-dialog/deposit-certificate-dialog.component.html create mode 100644 src/app/shared/deposit-certificate-dialog/deposit-certificate-dialog.component.scss create mode 100644 src/app/shared/deposit-certificate-dialog/deposit-certificate-dialog.component.ts diff --git a/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.html b/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.html index a0b81c891..d0ac260e9 100644 --- a/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.html +++ b/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.html @@ -33,6 +33,7 @@ @@ -107,7 +108,40 @@ - + + + + + Terms + +
+ @if (fixedDepositsAccountProductTemplate) { + + } + +
+ + + +
+
+
+ + @@ -121,6 +155,7 @@ [fixedDepositAccountData]="fixedDepositAccount" [selectedTenure]="selectedTenure" [fdNominees]="fixedDepositAccountCurrencyStep?.nominees ?? []" + [closureSettings]="fixedDepositAccountMaturityTermsStep?.closureSettings ?? {}" [hideButtons]="true" (submitEvent)="submit()" > diff --git a/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.ts b/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.ts index 99e4fcfef..d076cbf67 100644 --- a/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.ts +++ b/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.ts @@ -24,6 +24,7 @@ import { MatTabsModule } from '@angular/material/tabs'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { FixedDepositAccountInterestRateChartStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component'; import { FixedDepositAccountPreviewStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component'; +import { FixedDepositAccountMaturityTermsStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-maturity-terms-step/fixed-deposit-account-maturity-terms-step.component'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; /** @@ -40,6 +41,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; FixedDepositAccountDetailsStepComponent, FixedDepositAccountInterestRateChartStepComponent, FixedDepositAccountCurrencyStepComponent, + FixedDepositAccountMaturityTermsStepComponent, FixedDepositAccountPreviewStepComponent ] }) @@ -56,6 +58,10 @@ export class CreateFixedDepositAccountComponent { fixedDepositsAccountDetailsStep: FixedDepositAccountDetailsStepComponent; @ViewChild(FixedDepositAccountCurrencyStepComponent) fixedDepositAccountCurrencyStep: FixedDepositAccountCurrencyStepComponent; + @ViewChild(FixedDepositAccountPreviewStepComponent) + fixedDepositAccountPreviewStep: FixedDepositAccountPreviewStepComponent; + @ViewChild(FixedDepositAccountMaturityTermsStepComponent) + fixedDepositAccountMaturityTermsStep: FixedDepositAccountMaturityTermsStepComponent; /** Fixed Deposits Account Template */ fixedDepositsAccountTemplate: any; @@ -63,8 +69,8 @@ export class CreateFixedDepositAccountComponent { fixedDepositsAccountProductTemplate: any; /** Currently visible wizard tab (0-indexed). */ selectedTabIndex = 0; - /** Total wizard tabs — Product, Currency, Chart, Preview. */ - private readonly totalTabs = 4; + /** Total wizard tabs — Product, Currency, Chart, Terms, Preview. */ + private readonly totalTabs = 5; /** Active CBS base interest rate versions for the selected product's FD type */ cbsBaseRates: any[] = []; /** Tenure selected from the interest rate chart step */ @@ -243,6 +249,7 @@ export class CreateFixedDepositAccountComponent { ...this.fixedDepositAccount, ...terms, ...settings, + ...(this.fixedDepositAccountMaturityTermsStep?.closureSettings ?? {}), ...(this.selectedTenure ? { depositPeriod: this.selectedTenure.depositPeriod, diff --git a/src/app/deposits/fixed-deposits/edit-fixed-deposit-account/edit-fixed-deposit-account.component.html b/src/app/deposits/fixed-deposits/edit-fixed-deposit-account/edit-fixed-deposit-account.component.html index 8cbf3f2df..c7e98d743 100644 --- a/src/app/deposits/fixed-deposits/edit-fixed-deposit-account/edit-fixed-deposit-account.component.html +++ b/src/app/deposits/fixed-deposits/edit-fixed-deposit-account/edit-fixed-deposit-account.component.html @@ -35,6 +35,7 @@ @@ -111,7 +112,40 @@ - + + + + + Terms + +
+ @if (fixedDepositsAccountProductTemplate) { + + } + +
+ + + +
+
+
+ + @@ -125,6 +159,7 @@ [fixedDepositAccountData]="fixedDepositAccount" [selectedTenure]="selectedTenure" [fdNominees]="fixedDepositAccountCurrencyStep?.nominees ?? []" + [closureSettings]="fixedDepositAccountMaturityTermsStep?.closureSettings ?? {}" [hideButtons]="true" (submitEvent)="submit()" > diff --git a/src/app/deposits/fixed-deposits/edit-fixed-deposit-account/edit-fixed-deposit-account.component.ts b/src/app/deposits/fixed-deposits/edit-fixed-deposit-account/edit-fixed-deposit-account.component.ts index 7399b0efa..41c63cb0f 100644 --- a/src/app/deposits/fixed-deposits/edit-fixed-deposit-account/edit-fixed-deposit-account.component.ts +++ b/src/app/deposits/fixed-deposits/edit-fixed-deposit-account/edit-fixed-deposit-account.component.ts @@ -25,6 +25,7 @@ import { MatTabsModule } from '@angular/material/tabs'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { FixedDepositAccountInterestRateChartStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component'; import { FixedDepositAccountPreviewStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component'; +import { FixedDepositAccountMaturityTermsStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-maturity-terms-step/fixed-deposit-account-maturity-terms-step.component'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; /** @@ -41,6 +42,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; FixedDepositAccountDetailsStepComponent, FixedDepositAccountInterestRateChartStepComponent, FixedDepositAccountCurrencyStepComponent, + FixedDepositAccountMaturityTermsStepComponent, FixedDepositAccountPreviewStepComponent ] }) @@ -58,6 +60,10 @@ export class EditFixedDepositAccountComponent { fixedDepositsAccountDetailsStep: FixedDepositAccountDetailsStepComponent; @ViewChild(FixedDepositAccountCurrencyStepComponent) fixedDepositAccountCurrencyStep: FixedDepositAccountCurrencyStepComponent; + @ViewChild(FixedDepositAccountPreviewStepComponent) + fixedDepositAccountPreviewStep: FixedDepositAccountPreviewStepComponent; + @ViewChild(FixedDepositAccountMaturityTermsStepComponent) + fixedDepositAccountMaturityTermsStep: FixedDepositAccountMaturityTermsStepComponent; /** Full account data + template from resolver */ fixedDepositsAccountAndTemplate: any; @@ -65,7 +71,7 @@ export class EditFixedDepositAccountComponent { fixedDepositsAccountProductTemplate: any; /** Currently visible wizard tab (0-indexed). */ selectedTabIndex = 0; - private readonly totalTabs = 4; + private readonly totalTabs = 5; /** Active CBS base interest rate versions for the selected product's FD type */ cbsBaseRates: any[] = []; /** Tenure selected from the interest rate chart step */ @@ -236,6 +242,7 @@ export class EditFixedDepositAccountComponent { ...this.fixedDepositAccount, ...terms, ...settings, + ...(this.fixedDepositAccountMaturityTermsStep?.closureSettings ?? {}), ...(this.selectedTenure ? { depositPeriod: this.selectedTenure.depositPeriod, diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component.ts b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component.ts index c4a42c210..7aa178b82 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component.ts +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component.ts @@ -7,7 +7,17 @@ */ /** Angular Imports */ -import { Component, OnInit, Input, Output, EventEmitter, inject, ChangeDetectorRef } from '@angular/core'; +import { + Component, + OnInit, + OnChanges, + SimpleChanges, + Input, + Output, + EventEmitter, + inject, + ChangeDetectorRef +} from '@angular/core'; import { UntypedFormGroup, UntypedFormBuilder, Validators, FormsModule } from '@angular/forms'; import { SettingsService } from 'app/settings/settings.service'; import { Dates } from 'app/core/utils/dates'; @@ -43,7 +53,7 @@ import { IntroducerSearchComponent } from 'app/shared/introducer-search/introduc IntroducerSearchComponent ] }) -export class FixedDepositAccountDetailsStepComponent implements OnInit { +export class FixedDepositAccountDetailsStepComponent implements OnInit, OnChanges { private formBuilder = inject(UntypedFormBuilder); private fixedDepositsService = inject(FixedDepositsService); private settingsService = inject(SettingsService); @@ -56,6 +66,9 @@ export class FixedDepositAccountDetailsStepComponent implements OnInit { /** Fixed Deposits Account Template */ @Input() fixedDepositsAccountTemplate: any; @Input() hideButtons = false; + /** Linked-account id picked elsewhere in the wizard (e.g. the Terms step's Transfer to Savings + * account) -- pre-fills this card so the officer doesn't have to pick the same account twice. */ + @Input() externalLinkedAccountId: number | null = null; /** Minimum date allowed. */ minDate = new Date(2000, 0, 1); @@ -222,6 +235,23 @@ export class FixedDepositAccountDetailsStepComponent implements OnInit { this.dalSearch = ''; } + ngOnChanges(changes: SimpleChanges): void { + if (!changes.externalLinkedAccountId || this.selectedLinkedAccount) { + return; + } + const id = this.externalLinkedAccountId; + if (!id) return; + const savings = this.dalAllSavings.find((a: any) => a.id === id); + const current = this.dalAllCurrent.find((a: any) => a.id === id); + if (savings) { + this.dalSelectedType = 'SAVINGS'; + this.selectedLinkedAccount = savings; + } else if (current) { + this.dalSelectedType = 'CURRENT'; + this.selectedLinkedAccount = current; + } + } + private loadClientAccounts(): void { const clientId = String(this.fixedDepositsAccountTemplate?.clientId); if (!clientId) return; diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-maturity-terms-step/fixed-deposit-account-maturity-terms-step.component.html b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-maturity-terms-step/fixed-deposit-account-maturity-terms-step.component.html new file mode 100644 index 000000000..17d6f00d7 --- /dev/null +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-maturity-terms-step/fixed-deposit-account-maturity-terms-step.component.html @@ -0,0 +1,108 @@ + + +
+

{{ 'labels.heading.For Pre-mature closure' | translate }}

+ + + {{ 'labels.inputs.Maturity Instructions' | translate }} + + @for (maturityInstruction of maturityInstructionOptions; track maturityInstruction) { + + {{ maturityInstruction.value | translateKey: 'catalogs' }} + + } + + + + @if (closureSettingsForm.contains('transferToSavingsId')) { +
+
+
+
+ savings +
+
+

{{ 'labels.inputs.Transfer to savings account' | translate }}

+ Maturity proceeds will be credited to this account +
+
+
+ +
+ @if (selectedTransferAccount) { +
+
+ + savings + Savings + +
+ {{ selectedTransferAccount.accountNo }} + {{ selectedTransferAccount.savingsProductName }} +
+ @if (selectedTransferAccount.summary?.accountBalance != null) { +
+ Balance + + {{ selectedTransferAccount.currency?.code }} + {{ selectedTransferAccount.summary.accountBalance | number: '1.2-2' }} + +
+ } +
+ +
+ } @else { + + {{ 'labels.inputs.Transfer to savings account' | translate }} + + @for (account of savingsAccountsData; track account) { + + ({{ account.accountNo }}) {{ account.savingsProductName }} + + } + + Transfer to savings account is required + + } +
+
+ } + + + {{ 'labels.inputs.Apply Penal Interest (less)' | translate }} + + + @if (closureSettingsForm.value.preClosurePenalApplicable) { + + {{ 'labels.inputs.Penal Interest' | translate }} (%) + + + + + {{ 'labels.inputs.Period' | translate }} + + @for (preClosurePenalInterestOnType of preClosurePenalInterestOnTypeData; track preClosurePenalInterestOnType) { + + {{ preClosurePenalInterestOnType.value | translateKey: 'catalogs' }} + + } + + + } +
diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-maturity-terms-step/fixed-deposit-account-maturity-terms-step.component.scss b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-maturity-terms-step/fixed-deposit-account-maturity-terms-step.component.scss new file mode 100644 index 000000000..1f807bcce --- /dev/null +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-maturity-terms-step/fixed-deposit-account-maturity-terms-step.component.scss @@ -0,0 +1,152 @@ +/** + * 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/. + */ + +// ── Transfer-to-Savings account card (mirrors the Deposit Account Link card) ── + +.dal-card { + margin-top: 16px; + border: 1px solid #e2e8f0; + border-radius: 12px; + background: #fff; + overflow: hidden; + + &__header { + display: flex; + align-items: center; + gap: 14px; + padding: 16px 24px; + border-bottom: 1px solid #f1f5f9; + background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%); + } + + &__header-left { + display: flex; + align-items: center; + gap: 14px; + } + + &__icon { + width: 44px; + height: 44px; + border-radius: 10px; + background: #0ea5e9; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + } + + &__title { + margin: 0; + font-size: 15px; + font-weight: 700; + color: #0c4a6e; + } + + &__sub { + display: block; + font-size: 12px; + color: #64748b; + margin-top: 2px; + } + + &__body { + padding: 20px 24px; + } +} + +.dal-selected { + display: flex; + align-items: center; + gap: 16px; + padding: 16px 20px; + border: 2px solid #0ea5e9; + border-radius: 12px; + background: #f0f9ff; + + &__info { + display: flex; + align-items: center; + gap: 14px; + flex: 1; + flex-wrap: wrap; + } + + &__details { + display: flex; + flex-direction: column; + gap: 2px; + flex: 1; + } + + &__accno { + font-size: 15px; + font-weight: 700; + color: #0c4a6e; + font-family: monospace; + } + + &__product { + font-size: 12px; + color: #64748b; + } + + &__balance { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 2px; + } + + &__bal-label { + font-size: 10px; + color: #94a3b8; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + &__bal-amount { + font-size: 15px; + font-weight: 700; + color: #0c4a6e; + } +} + +.dal-change-btn { + white-space: nowrap; +} + +.dal-type-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 12px; + border-radius: 999px; + font-size: 12px; + font-weight: 700; + white-space: nowrap; + background: #f1f5f9; + color: #475569; + border: 1px solid #e2e8f0; + + mat-icon { + font-size: 14px; + width: 14px; + height: 14px; + } + + &--savings { + background: #eff6ff; + color: #1d4ed8; + border-color: #bfdbfe; + + mat-icon { + color: #2563eb; + } + } +} diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-maturity-terms-step/fixed-deposit-account-maturity-terms-step.component.ts b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-maturity-terms-step/fixed-deposit-account-maturity-terms-step.component.ts new file mode 100644 index 000000000..bce8468a1 --- /dev/null +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-maturity-terms-step/fixed-deposit-account-maturity-terms-step.component.ts @@ -0,0 +1,117 @@ +/** + * 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, Input, OnChanges, SimpleChanges, inject } from '@angular/core'; +import { UntypedFormGroup, UntypedFormBuilder, UntypedFormControl, Validators } from '@angular/forms'; +import { MatCheckbox } from '@angular/material/checkbox'; +import { MatIcon } from '@angular/material/icon'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * Fixed Deposit Account Maturity Terms Step -- what happens to this account at maturity + * (Withdraw / Transfer to Savings / Reinvest) and the pre-mature-closure penalty, decided + * per account rather than silently inherited from the product. + */ +@Component({ + selector: 'mifosx-fixed-deposit-account-maturity-terms-step', + templateUrl: './fixed-deposit-account-maturity-terms-step.component.html', + styleUrls: ['./fixed-deposit-account-maturity-terms-step.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatCheckbox, + MatIcon + ] +}) +export class FixedDepositAccountMaturityTermsStepComponent implements OnChanges { + private formBuilder = inject(UntypedFormBuilder); + + @Input() fixedDepositsAccountTemplate: any; + @Input() fixedDepositsAccountProductTemplate: any; + /** Account already linked elsewhere in the wizard (the Details step's Deposit Account Link) -- + * pre-fills Transfer to Savings so the officer doesn't have to pick the same account twice. */ + @Input() linkedSavingsAccountId: number | null = null; + + closureSettingsForm: UntypedFormGroup; + maturityInstructionOptions: any[] = []; + savingsAccountsData: any[] = []; + preClosurePenalInterestOnTypeData: any[] = []; + + constructor() { + this.closureSettingsForm = this.formBuilder.group({ + maturityInstructionId: [''], + preClosurePenalApplicable: [false], + preClosurePenalInterest: [ + '', + Validators.min(0) + ], + preClosurePenalInterestOnTypeId: [''] + }); + this.closureSettingsForm.get('maturityInstructionId').valueChanges.subscribe((value: number) => { + // 200 = Transfer to Savings -- Withdraw Deposit (100) needs no target account, and both + // Reinvest options (300/400) open a brand-new FD instead, so they ignore this field too. + if (value === 200) { + this.closureSettingsForm.addControl( + 'transferToSavingsId', + new UntypedFormControl(this.linkedSavingsAccountId ?? '', Validators.required) + ); + } else { + this.closureSettingsForm.removeControl('transferToSavingsId'); + } + }); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.linkedSavingsAccountId) { + const control = this.closureSettingsForm.get('transferToSavingsId'); + if (control && !control.value && this.linkedSavingsAccountId) { + control.setValue(this.linkedSavingsAccountId); + } + } + if (this.fixedDepositsAccountProductTemplate) { + // Withdraw Deposit (id 100) books the maturity proceeds out of the bank's cash/vault the moment + // the account matures, before the customer has actually collected it -- disabled everywhere. + this.maturityInstructionOptions = ( + this.fixedDepositsAccountProductTemplate.maturityInstructionOptions ?? [] + ).filter((option: any) => option.id !== 100); + this.savingsAccountsData = this.fixedDepositsAccountProductTemplate.savingsAccounts ?? []; + this.preClosurePenalInterestOnTypeData = + this.fixedDepositsAccountProductTemplate.preClosurePenalInterestOnTypeOptions ?? []; + if (!this.closureSettingsForm.get('preClosurePenalApplicable').dirty) { + this.closureSettingsForm.patchValue({ + preClosurePenalApplicable: this.fixedDepositsAccountProductTemplate.preClosurePenalApplicable ?? false, + preClosurePenalInterest: this.fixedDepositsAccountProductTemplate.preClosurePenalInterest ?? '', + preClosurePenalInterestOnTypeId: + this.fixedDepositsAccountProductTemplate.preClosurePenalInterestOnType?.id ?? '' + }); + } + } + } + + /** The linked-savings account for the transfer-to-savings maturity instruction, once picked. */ + get selectedTransferAccount(): any { + const id = this.closureSettingsForm.value.transferToSavingsId; + if (!id) return null; + return this.savingsAccountsData.find((account: any) => account.id === id) ?? null; + } + + clearTransferAccount(): void { + this.closureSettingsForm.get('transferToSavingsId')?.setValue(''); + } + + /** Chosen maturity/closure settings, ready to merge into the submit payload. */ + get closureSettings() { + const settings = this.closureSettingsForm.getRawValue(); + for (const key in settings) { + if (settings[key] === '') { + delete settings[key]; + } + } + return settings; + } +} diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component.html b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component.html index 7abd9466a..98644bdb4 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component.html +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component.html @@ -683,6 +683,41 @@ +
+
+ + Maturity & Pre-mature Closure +
+
+
+
+ {{ 'labels.inputs.Maturity Instructions' | translate }} + {{ closureMaturityInstructionLabel }} +
+ @if (closureSettings?.transferToSavingsId) { +
+ {{ 'labels.inputs.Transfer to savings account' | translate }} + {{ closureTransferToSavingsLabel }} +
+ } +
+ {{ 'labels.inputs.Apply Penal Interest (less)' | translate }} + {{ closureSettings?.preClosurePenalApplicable | yesNo }} +
+ @if (closureSettings?.preClosurePenalApplicable) { +
+ {{ 'labels.inputs.Penal Interest' | translate }} (%) + {{ closureSettings?.preClosurePenalInterest }}% +
+
+ {{ 'labels.inputs.Period' | translate }} + {{ closurePenalInterestOnTypeLabel }} +
+ } +
+
+
+ @if (!hideButtons) {
+ } 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 7715b65c5..0e550afb3 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 @@ -16,6 +16,7 @@ import { ConfirmationDialogComponent } from 'app/shared/confirmation-dialog/conf import { CalculateInterestDialogComponent } from './custom-dialogs/calculate-interest-dialog/calculate-interest-dialog.component'; 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 { DepositCertificateDialogComponent } from 'app/shared/deposit-certificate-dialog/deposit-certificate-dialog.component'; /** Custom Button Config. */ import { FixedDepositsButtonsConfiguration } from './fixed-deposits-buttons.config'; @@ -358,6 +359,20 @@ export class FixedDepositAccountViewComponent implements OnInit { }); } + /** + * Opens a printable deposit certificate for this account. + */ + printCertificate() { + this.dialog.open(DepositCertificateDialogComponent, { + width: '900px', + data: { + accountType: 'FD', + accountData: this.fixedDepositsAccountData, + clientId: this.fixedDepositsAccountData.clientId + } + }); + } + /** * Disables withhold tax for fixed deposits account * Fixed deposits endpoint is not supported so using Savings endpoint. diff --git a/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.html b/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.html index d4f2d0b6c..7b55c02bd 100644 --- a/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.html +++ b/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.html @@ -33,6 +33,7 @@ @@ -111,7 +112,40 @@
- + + + + + Terms + +
+ @if (recurringDepositsAccountProductTemplate) { + + } + +
+ + + +
+
+
+ + @@ -132,6 +166,7 @@ [rdDaysInYear]="rdDaysInYear" [rdAppliedRate]="rdAppliedRate" [rdIsSeniorCitizen]="rdIsSeniorCitizen" + [closureSettings]="recurringDepositsAccountMaturityTermsStep?.closureSettings ?? {}" [hideButtons]="true" (submitEvent)="submit()" > diff --git a/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.ts b/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.ts index 8ab2a000c..d68fd0229 100644 --- a/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.ts +++ b/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.ts @@ -24,6 +24,7 @@ import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { RecurringDepositsAccountCurrencyStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component'; import { RecurringDepositsAccountInterestRateChartStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component'; import { RecurringDepositsAccountPreviewStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component'; +import { RecurringDepositsAccountMaturityTermsStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-maturity-terms-step/recurring-deposits-account-maturity-terms-step.component'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; /** @@ -40,6 +41,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; RecurringDepositsAccountDetailsStepComponent, RecurringDepositsAccountCurrencyStepComponent, RecurringDepositsAccountInterestRateChartStepComponent, + RecurringDepositsAccountMaturityTermsStepComponent, RecurringDepositsAccountPreviewStepComponent ] }) @@ -61,6 +63,12 @@ export class CreateRecurringDepositsAccountComponent { @ViewChild(RecurringDepositsAccountInterestRateChartStepComponent) recurringDepositsAccountChartStep: RecurringDepositsAccountInterestRateChartStepComponent; + @ViewChild(RecurringDepositsAccountPreviewStepComponent) + recurringDepositsAccountPreviewStep: RecurringDepositsAccountPreviewStepComponent; + + @ViewChild(RecurringDepositsAccountMaturityTermsStepComponent) + recurringDepositsAccountMaturityTermsStep: RecurringDepositsAccountMaturityTermsStepComponent; + /** Recurring Deposits Account Template */ recurringDepositsAccountTemplate: any; /** Recurring Deposit Account Product Template */ @@ -68,7 +76,7 @@ export class CreateRecurringDepositsAccountComponent { /** Currently visible wizard tab (0-indexed). */ selectedTabIndex = 0; /** Total wizard tabs — Product, Currency, Chart, Preview. */ - private readonly totalTabs = 4; + private readonly totalTabs = 5; /** Active CBS base interest rate versions */ cbsBaseRates: any[] = []; /** Selected tenure value and frequency from calculator */ @@ -258,6 +266,7 @@ export class CreateRecurringDepositsAccountComponent { ...details, ...terms, ...settings, + ...(this.recurringDepositsAccountMaturityTermsStep?.closureSettings ?? {}), ...productDefaults, clientId: this.recurringDepositsAccountTemplate.clientId, mandatoryRecommendedDepositAmount: pt?.minDepositAmount ?? 0, diff --git a/src/app/deposits/recurring-deposits/edit-recurring-deposit-account/edit-recurring-deposit-account.component.html b/src/app/deposits/recurring-deposits/edit-recurring-deposit-account/edit-recurring-deposit-account.component.html index f3d7d5225..e03d3542d 100644 --- a/src/app/deposits/recurring-deposits/edit-recurring-deposit-account/edit-recurring-deposit-account.component.html +++ b/src/app/deposits/recurring-deposits/edit-recurring-deposit-account/edit-recurring-deposit-account.component.html @@ -35,6 +35,7 @@ @@ -115,7 +116,40 @@ - + + + + + Terms + +
+ @if (recurringDepositsAccountProductTemplate) { + + } + +
+ + + +
+
+
+ + @@ -136,6 +170,7 @@ [rdDaysInYear]="rdDaysInYear" [rdAppliedRate]="rdAppliedRate" [rdIsSeniorCitizen]="rdIsSeniorCitizen" + [closureSettings]="recurringDepositsAccountMaturityTermsStep?.closureSettings ?? {}" [hideButtons]="true" (submitEvent)="submit()" > diff --git a/src/app/deposits/recurring-deposits/edit-recurring-deposit-account/edit-recurring-deposit-account.component.ts b/src/app/deposits/recurring-deposits/edit-recurring-deposit-account/edit-recurring-deposit-account.component.ts index 4076b4661..7b5bb33f3 100644 --- a/src/app/deposits/recurring-deposits/edit-recurring-deposit-account/edit-recurring-deposit-account.component.ts +++ b/src/app/deposits/recurring-deposits/edit-recurring-deposit-account/edit-recurring-deposit-account.component.ts @@ -25,6 +25,7 @@ import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { RecurringDepositsAccountCurrencyStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component'; import { RecurringDepositsAccountInterestRateChartStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component'; import { RecurringDepositsAccountPreviewStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component'; +import { RecurringDepositsAccountMaturityTermsStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-maturity-terms-step/recurring-deposits-account-maturity-terms-step.component'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; /** @@ -41,6 +42,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; RecurringDepositsAccountDetailsStepComponent, RecurringDepositsAccountCurrencyStepComponent, RecurringDepositsAccountInterestRateChartStepComponent, + RecurringDepositsAccountMaturityTermsStepComponent, RecurringDepositsAccountPreviewStepComponent ] }) @@ -63,13 +65,19 @@ export class EditRecurringDepositAccountComponent { @ViewChild(RecurringDepositsAccountInterestRateChartStepComponent) recurringDepositsAccountChartStep: RecurringDepositsAccountInterestRateChartStepComponent; + @ViewChild(RecurringDepositsAccountPreviewStepComponent) + recurringDepositsAccountPreviewStep: RecurringDepositsAccountPreviewStepComponent; + + @ViewChild(RecurringDepositsAccountMaturityTermsStepComponent) + recurringDepositsAccountMaturityTermsStep: RecurringDepositsAccountMaturityTermsStepComponent; + /** Full account data + template from resolver */ recurringDepositsAccountAndTemplate: any; /** Product template emitted by details step */ recurringDepositsAccountProductTemplate: any; /** Currently visible wizard tab (0-indexed). */ selectedTabIndex = 0; - private readonly totalTabs = 4; + private readonly totalTabs = 5; /** Active CBS base interest rate versions */ cbsBaseRates: any[] = []; /** Selected tenure value and frequency from calculator */ @@ -277,6 +285,7 @@ export class EditRecurringDepositAccountComponent { ...details, ...terms, ...settings, + ...(this.recurringDepositsAccountMaturityTermsStep?.closureSettings ?? {}), ...productDefaults, clientId: this.recurringDepositsAccountAndTemplate.clientId, mandatoryRecommendedDepositAmount: pt?.minDepositAmount ?? 0, diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts index 6ba4bea37..f7bb9d68f 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts @@ -7,7 +7,17 @@ */ /** Angular Imports */ -import { Component, OnInit, Input, Output, EventEmitter, inject, ChangeDetectorRef } from '@angular/core'; +import { + Component, + OnInit, + OnChanges, + SimpleChanges, + Input, + Output, + EventEmitter, + inject, + ChangeDetectorRef +} from '@angular/core'; import { UntypedFormGroup, UntypedFormBuilder, Validators, FormsModule } from '@angular/forms'; import { SettingsService } from 'app/settings/settings.service'; import { Dates } from 'app/core/utils/dates'; @@ -43,7 +53,7 @@ import { IntroducerSearchComponent } from 'app/shared/introducer-search/introduc IntroducerSearchComponent ] }) -export class RecurringDepositsAccountDetailsStepComponent implements OnInit { +export class RecurringDepositsAccountDetailsStepComponent implements OnInit, OnChanges { private formBuilder = inject(UntypedFormBuilder); private recurringDepositsService = inject(RecurringDepositsService); private settingsService = inject(SettingsService); @@ -56,6 +66,10 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { /** Recurring Deposits Account Template */ @Input() recurringDepositsAccountTemplate: any; + /** Linked-account id picked elsewhere in the wizard (e.g. the Terms step's Transfer to Savings + * account) -- pre-fills this card so the officer doesn't have to pick the same account twice. */ + @Input() externalLinkedAccountId: number | null = null; + /** Minimum date allowed. */ minDate = new Date(2000, 0, 1); /** Maximum date allowed. */ @@ -198,6 +212,23 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { this.dalSearch = ''; } + ngOnChanges(changes: SimpleChanges): void { + if (!changes.externalLinkedAccountId || this.selectedLinkedAccount) { + return; + } + const id = this.externalLinkedAccountId; + if (!id) return; + const savings = this.dalAllSavings.find((a: any) => a.id === id); + const current = this.dalAllCurrent.find((a: any) => a.id === id); + if (savings) { + this.dalSelectedType = 'SAVINGS'; + this.selectedLinkedAccount = savings; + } else if (current) { + this.dalSelectedType = 'CURRENT'; + this.selectedLinkedAccount = current; + } + } + private loadClientAccounts(): void { const clientId = String(this.recurringDepositsAccountTemplate?.clientId); if (!clientId) return; diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-maturity-terms-step/recurring-deposits-account-maturity-terms-step.component.html b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-maturity-terms-step/recurring-deposits-account-maturity-terms-step.component.html new file mode 100644 index 000000000..17d6f00d7 --- /dev/null +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-maturity-terms-step/recurring-deposits-account-maturity-terms-step.component.html @@ -0,0 +1,108 @@ + + +
+

{{ 'labels.heading.For Pre-mature closure' | translate }}

+ + + {{ 'labels.inputs.Maturity Instructions' | translate }} + + @for (maturityInstruction of maturityInstructionOptions; track maturityInstruction) { + + {{ maturityInstruction.value | translateKey: 'catalogs' }} + + } + + + + @if (closureSettingsForm.contains('transferToSavingsId')) { +
+
+
+
+ savings +
+
+

{{ 'labels.inputs.Transfer to savings account' | translate }}

+ Maturity proceeds will be credited to this account +
+
+
+ +
+ @if (selectedTransferAccount) { +
+
+ + savings + Savings + +
+ {{ selectedTransferAccount.accountNo }} + {{ selectedTransferAccount.savingsProductName }} +
+ @if (selectedTransferAccount.summary?.accountBalance != null) { +
+ Balance + + {{ selectedTransferAccount.currency?.code }} + {{ selectedTransferAccount.summary.accountBalance | number: '1.2-2' }} + +
+ } +
+ +
+ } @else { + + {{ 'labels.inputs.Transfer to savings account' | translate }} + + @for (account of savingsAccountsData; track account) { + + ({{ account.accountNo }}) {{ account.savingsProductName }} + + } + + Transfer to savings account is required + + } +
+
+ } + + + {{ 'labels.inputs.Apply Penal Interest (less)' | translate }} + + + @if (closureSettingsForm.value.preClosurePenalApplicable) { + + {{ 'labels.inputs.Penal Interest' | translate }} (%) + + + + + {{ 'labels.inputs.Period' | translate }} + + @for (preClosurePenalInterestOnType of preClosurePenalInterestOnTypeData; track preClosurePenalInterestOnType) { + + {{ preClosurePenalInterestOnType.value | translateKey: 'catalogs' }} + + } + + + } +
diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-maturity-terms-step/recurring-deposits-account-maturity-terms-step.component.scss b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-maturity-terms-step/recurring-deposits-account-maturity-terms-step.component.scss new file mode 100644 index 000000000..1f807bcce --- /dev/null +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-maturity-terms-step/recurring-deposits-account-maturity-terms-step.component.scss @@ -0,0 +1,152 @@ +/** + * 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/. + */ + +// ── Transfer-to-Savings account card (mirrors the Deposit Account Link card) ── + +.dal-card { + margin-top: 16px; + border: 1px solid #e2e8f0; + border-radius: 12px; + background: #fff; + overflow: hidden; + + &__header { + display: flex; + align-items: center; + gap: 14px; + padding: 16px 24px; + border-bottom: 1px solid #f1f5f9; + background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%); + } + + &__header-left { + display: flex; + align-items: center; + gap: 14px; + } + + &__icon { + width: 44px; + height: 44px; + border-radius: 10px; + background: #0ea5e9; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + } + + &__title { + margin: 0; + font-size: 15px; + font-weight: 700; + color: #0c4a6e; + } + + &__sub { + display: block; + font-size: 12px; + color: #64748b; + margin-top: 2px; + } + + &__body { + padding: 20px 24px; + } +} + +.dal-selected { + display: flex; + align-items: center; + gap: 16px; + padding: 16px 20px; + border: 2px solid #0ea5e9; + border-radius: 12px; + background: #f0f9ff; + + &__info { + display: flex; + align-items: center; + gap: 14px; + flex: 1; + flex-wrap: wrap; + } + + &__details { + display: flex; + flex-direction: column; + gap: 2px; + flex: 1; + } + + &__accno { + font-size: 15px; + font-weight: 700; + color: #0c4a6e; + font-family: monospace; + } + + &__product { + font-size: 12px; + color: #64748b; + } + + &__balance { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 2px; + } + + &__bal-label { + font-size: 10px; + color: #94a3b8; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + &__bal-amount { + font-size: 15px; + font-weight: 700; + color: #0c4a6e; + } +} + +.dal-change-btn { + white-space: nowrap; +} + +.dal-type-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 12px; + border-radius: 999px; + font-size: 12px; + font-weight: 700; + white-space: nowrap; + background: #f1f5f9; + color: #475569; + border: 1px solid #e2e8f0; + + mat-icon { + font-size: 14px; + width: 14px; + height: 14px; + } + + &--savings { + background: #eff6ff; + color: #1d4ed8; + border-color: #bfdbfe; + + mat-icon { + color: #2563eb; + } + } +} diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-maturity-terms-step/recurring-deposits-account-maturity-terms-step.component.ts b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-maturity-terms-step/recurring-deposits-account-maturity-terms-step.component.ts new file mode 100644 index 000000000..d32140665 --- /dev/null +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-maturity-terms-step/recurring-deposits-account-maturity-terms-step.component.ts @@ -0,0 +1,116 @@ +/** + * 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, Input, OnChanges, SimpleChanges, inject } from '@angular/core'; +import { UntypedFormGroup, UntypedFormBuilder, UntypedFormControl, Validators } from '@angular/forms'; +import { MatCheckbox } from '@angular/material/checkbox'; +import { MatIcon } from '@angular/material/icon'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * Recurring Deposit Account Maturity Terms Step -- what happens to this account at maturity + * (Withdraw / Transfer to Savings / Reinvest) and the pre-mature-closure penalty, decided + * per account rather than silently inherited from the product. + */ +@Component({ + selector: 'mifosx-recurring-deposits-account-maturity-terms-step', + templateUrl: './recurring-deposits-account-maturity-terms-step.component.html', + styleUrls: ['./recurring-deposits-account-maturity-terms-step.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatCheckbox, + MatIcon + ] +}) +export class RecurringDepositsAccountMaturityTermsStepComponent implements OnChanges { + private formBuilder = inject(UntypedFormBuilder); + + @Input() recurringDepositsAccountTemplate: any; + @Input() recurringDepositsAccountProductTemplate: any; + /** Account already linked elsewhere in the wizard (the Details step's Deposit Account Link) -- + * pre-fills Transfer to Savings so the officer doesn't have to pick the same account twice. */ + @Input() linkedSavingsAccountId: number | null = null; + + closureSettingsForm: UntypedFormGroup; + maturityInstructionOptions: any[] = []; + savingsAccountsData: any[] = []; + preClosurePenalInterestOnTypeData: any[] = []; + + constructor() { + this.closureSettingsForm = this.formBuilder.group({ + maturityInstructionId: [''], + preClosurePenalApplicable: [false], + preClosurePenalInterest: [ + '', + Validators.min(0) + ], + preClosurePenalInterestOnTypeId: [''] + }); + this.closureSettingsForm.get('maturityInstructionId').valueChanges.subscribe((value: number) => { + // 200 = Transfer to Savings -- Withdraw Deposit (100) needs no target account, and both + // Reinvest options (300/400) open a brand-new RD instead, so they ignore this field too. + if (value === 200) { + this.closureSettingsForm.addControl( + 'transferToSavingsId', + new UntypedFormControl(this.linkedSavingsAccountId ?? '', Validators.required) + ); + } else { + this.closureSettingsForm.removeControl('transferToSavingsId'); + } + }); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.linkedSavingsAccountId) { + const control = this.closureSettingsForm.get('transferToSavingsId'); + if (control && !control.value && this.linkedSavingsAccountId) { + control.setValue(this.linkedSavingsAccountId); + } + } + const template = this.recurringDepositsAccountProductTemplate || this.recurringDepositsAccountTemplate; + if (template) { + // Withdraw Deposit (id 100) books the maturity proceeds out of the bank's cash/vault the moment + // the account matures, before the customer has actually collected it -- disabled everywhere. + this.maturityInstructionOptions = (template.maturityInstructionOptions ?? []).filter( + (option: any) => option.id !== 100 + ); + this.savingsAccountsData = template.savingsAccounts ?? []; + this.preClosurePenalInterestOnTypeData = template.preClosurePenalInterestOnTypeOptions ?? []; + if (!this.closureSettingsForm.get('preClosurePenalApplicable').dirty) { + this.closureSettingsForm.patchValue({ + preClosurePenalApplicable: template.preClosurePenalApplicable ?? false, + preClosurePenalInterest: template.preClosurePenalInterest ?? '', + preClosurePenalInterestOnTypeId: template.preClosurePenalInterestOnType?.id ?? '' + }); + } + } + } + + /** The linked-savings account for the transfer-to-savings maturity instruction, once picked. */ + get selectedTransferAccount(): any { + const id = this.closureSettingsForm.value.transferToSavingsId; + if (!id) return null; + return this.savingsAccountsData.find((account: any) => account.id === id) ?? null; + } + + clearTransferAccount(): void { + this.closureSettingsForm.get('transferToSavingsId')?.setValue(''); + } + + /** Chosen maturity/closure settings, ready to merge into the submit payload. */ + get closureSettings() { + const settings = this.closureSettingsForm.getRawValue(); + for (const key in settings) { + if (settings[key] === '') { + delete settings[key]; + } + } + return settings; + } +} diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.html b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.html index 2ff42a561..4abb78a1a 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.html +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.html @@ -853,6 +853,41 @@ } +
+
+ + Maturity & Pre-mature Closure +
+
+
+
+ {{ 'labels.inputs.Maturity Instructions' | translate }} + {{ closureMaturityInstructionLabel }} +
+ @if (closureSettings?.transferToSavingsId) { +
+ {{ 'labels.inputs.Transfer to savings account' | translate }} + {{ closureTransferToSavingsLabel }} +
+ } +
+ {{ 'labels.inputs.Apply Penal Interest (less)' | translate }} + {{ closureSettings?.preClosurePenalApplicable | yesNo }} +
+ @if (closureSettings?.preClosurePenalApplicable) { +
+ {{ 'labels.inputs.Penal Interest' | translate }} (%) + {{ closureSettings?.preClosurePenalInterest }}% +
+
+ {{ 'labels.inputs.Period' | translate }} + {{ closurePenalInterestOnTypeLabel }} +
+ } +
+
+
+ @if (!hideButtons) {
+ } diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-view/recurring-deposits-account-view.component.ts b/src/app/deposits/recurring-deposits/recurring-deposits-account-view/recurring-deposits-account-view.component.ts index 24ca434e9..22d3aa3da 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-view/recurring-deposits-account-view.component.ts +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-view/recurring-deposits-account-view.component.ts @@ -22,6 +22,7 @@ import { RecurringDepositsButtonsConfiguration } from './recurring-deposits-butt import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component'; import { ConfirmationDialogComponent } from 'app/shared/confirmation-dialog/confirmation-dialog.component'; import { RecurringDepositConfirmationDialogComponent } from './custom-dialogs/recurring-deposit-confirmation-dialog/recurring-deposit-confirmation-dialog.component'; +import { DepositCertificateDialogComponent } from 'app/shared/deposit-certificate-dialog/deposit-certificate-dialog.component'; import { Currency } from 'app/shared/models/general.model'; import { TranslateService } from '@ngx-translate/core'; import { AccountsService } from 'app/accounts/accounts.service'; @@ -426,6 +427,20 @@ export class RecurringDepositsAccountViewComponent implements OnInit { }); } + /** + * Opens a printable deposit certificate for this account. + */ + printCertificate() { + this.dialog.open(DepositCertificateDialogComponent, { + width: '900px', + data: { + accountType: 'RD', + accountData: this.recurringDepositsAccountData, + clientId: this.recurringDepositsAccountData.clientId + } + }); + } + /** * Disables withhold tax for recurring deposits account * Recurring deposits endpoint is not supported so using Savings endpoint. diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-settings-step/fixed-deposit-product-settings-step.component.html b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-settings-step/fixed-deposit-product-settings-step.component.html index 7843f2654..201b48241 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-settings-step/fixed-deposit-product-settings-step.component.html +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-settings-step/fixed-deposit-product-settings-step.component.html @@ -21,6 +21,33 @@ Lien Marked Apply Withdrawal Fee for Transfers
+ + + {{ 'labels.inputs.Apply Penal Interest (less)' | translate }} + + + @if (fixedDepositProductSettingsForm.value.preClosurePenalApplicable) { +
+ + {{ 'labels.inputs.Penal Interest' | translate }} (%) + + + + + {{ 'labels.inputs.Period' | translate }} + + @for ( + preClosurePenalInterestOnType of preClosurePenalInterestOnTypeData; + track preClosurePenalInterestOnType + ) { + + {{ preClosurePenalInterestOnType.value | translateKey: 'catalogs' }} + + } + + +
+ } diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-settings-step/fixed-deposit-product-settings-step.component.scss b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-settings-step/fixed-deposit-product-settings-step.component.scss index 8310cf654..05386d6d6 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-settings-step/fixed-deposit-product-settings-step.component.scss +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-settings-step/fixed-deposit-product-settings-step.component.scss @@ -130,6 +130,15 @@ $gray-900: #202124; } } +.fdps-preclosure-toggle { + display: block; + margin: 4px 0 16px; + padding-bottom: 16px; + border-bottom: 1px solid $gray-200; + font-size: 14px; + color: $gray-900; +} + .fdps-layout { display: grid; gap: 16px; diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-settings-step/fixed-deposit-product-settings-step.component.ts b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-settings-step/fixed-deposit-product-settings-step.component.ts index 33933731c..fadc1ce65 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-settings-step/fixed-deposit-product-settings-step.component.ts +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-settings-step/fixed-deposit-product-settings-step.component.ts @@ -30,6 +30,7 @@ export class FixedDepositProductSettingsStepComponent implements OnInit { fixedDepositProductSettingsForm: UntypedFormGroup; lockinPeriodFrequencyTypeData: any; + preClosurePenalInterestOnTypeData: any; constructor() { this.createFixedDepositProductSettingsForm(); @@ -37,6 +38,7 @@ export class FixedDepositProductSettingsStepComponent implements OnInit { ngOnInit() { this.lockinPeriodFrequencyTypeData = this.fixedDepositProductsTemplate.lockinPeriodFrequencyTypeOptions; + this.preClosurePenalInterestOnTypeData = this.fixedDepositProductsTemplate.preClosurePenalInterestOnTypeOptions; if (this.fixedDepositProductsTemplate?.id) { this.fixedDepositProductSettingsForm.patchValue({ @@ -48,6 +50,9 @@ export class FixedDepositProductSettingsStepComponent implements OnInit { enableLockinPeriod: this.fixedDepositProductsTemplate.enableLockinPeriod ?? false, lockinPeriodFrequency: this.fixedDepositProductsTemplate.lockinPeriodFrequency ?? '', lockinPeriodFrequencyType: this.fixedDepositProductsTemplate.lockinPeriodFrequencyType?.id ?? '', + preClosurePenalApplicable: this.fixedDepositProductsTemplate.preClosurePenalApplicable ?? false, + preClosurePenalInterest: this.fixedDepositProductsTemplate.preClosurePenalInterest ?? '', + preClosurePenalInterestOnTypeId: this.fixedDepositProductsTemplate.preClosurePenalInterestOnType?.id ?? '', maturityInstructions: this.fixedDepositProductsTemplate.maturityInstructions ?? false, creditMaturityProceeds: this.fixedDepositProductsTemplate.creditMaturityProceeds ?? false, penaltyGracePeriodMissedInstallments: @@ -76,6 +81,12 @@ export class FixedDepositProductSettingsStepComponent implements OnInit { Validators.min(0) ], lockinPeriodFrequencyType: [''], + preClosurePenalApplicable: [false], + preClosurePenalInterest: [ + '', + Validators.min(0) + ], + preClosurePenalInterestOnTypeId: [''], maturityInstructions: [false], creditMaturityProceeds: [false], penaltyGracePeriodMissedInstallments: [false], diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component.html b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component.html index ca817dbd2..674879dc3 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component.html +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component.html @@ -21,6 +21,33 @@ Lien Marked Apply Withdrawal Fee for Transfers + + + {{ 'labels.inputs.Apply Penal Interest (less)' | translate }} + + + @if (recurringDepositProductSettingsForm.value.preClosurePenalApplicable) { +
+ + {{ 'labels.inputs.Penal Interest' | translate }} (%) + + + + + {{ 'labels.inputs.Period' | translate }} + + @for ( + preClosurePenalInterestOnType of preClosurePenalInterestOnTypeData; + track preClosurePenalInterestOnType + ) { + + {{ preClosurePenalInterestOnType.value | translateKey: 'catalogs' }} + + } + + +
+ } diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component.scss b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component.scss index 3aefca054..a21434a6b 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component.scss +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component.scss @@ -162,3 +162,12 @@ $gray-900: #202124; } } } + +.rdps-preclosure-toggle { + display: block; + margin: 4px 0 16px; + padding-bottom: 16px; + border-bottom: 1px solid $gray-200; + font-size: 14px; + color: $gray-900; +} diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component.ts b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component.ts index 74c8ca844..6a336eed0 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component.ts +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component.ts @@ -28,6 +28,7 @@ export class RecurringDepositProductSettingsStepComponent implements OnInit { recurringDepositProductSettingsForm: UntypedFormGroup; lockinPeriodFrequencyTypeData: any; + preClosurePenalInterestOnTypeData: any; constructor() { this.createrecurringDepositProductSettingsForm(); @@ -35,6 +36,7 @@ export class RecurringDepositProductSettingsStepComponent implements OnInit { ngOnInit() { this.lockinPeriodFrequencyTypeData = this.recurringDepositProductsTemplate.lockinPeriodFrequencyTypeOptions; + this.preClosurePenalInterestOnTypeData = this.recurringDepositProductsTemplate.preClosurePenalInterestOnTypeOptions; if (this.recurringDepositProductsTemplate?.id) { this.recurringDepositProductSettingsForm.patchValue({ @@ -46,6 +48,9 @@ export class RecurringDepositProductSettingsStepComponent implements OnInit { enableLockinPeriod: this.recurringDepositProductsTemplate.enableLockinPeriod ?? false, lockinPeriodFrequency: this.recurringDepositProductsTemplate.lockinPeriodFrequency ?? '', lockinPeriodFrequencyType: this.recurringDepositProductsTemplate.lockinPeriodFrequencyType?.id ?? '', + preClosurePenalApplicable: this.recurringDepositProductsTemplate.preClosurePenalApplicable ?? false, + preClosurePenalInterest: this.recurringDepositProductsTemplate.preClosurePenalInterest ?? '', + preClosurePenalInterestOnTypeId: this.recurringDepositProductsTemplate.preClosurePenalInterestOnType?.id ?? '', maturityInstructions: this.recurringDepositProductsTemplate.maturityInstructions ?? false, creditMaturityProceeds: this.recurringDepositProductsTemplate.creditMaturityProceeds ?? false, penaltyGracePeriodMissedInstallments: @@ -71,6 +76,9 @@ export class RecurringDepositProductSettingsStepComponent implements OnInit { enableLockinPeriod: [false], lockinPeriodFrequency: [''], lockinPeriodFrequencyType: [''], + preClosurePenalApplicable: [false], + preClosurePenalInterest: [''], + preClosurePenalInterestOnTypeId: [''], maturityInstructions: [false], creditMaturityProceeds: [false], penaltyGracePeriodMissedInstallments: [false], diff --git a/src/app/shared/deposit-certificate-dialog/deposit-certificate-dialog.component.html b/src/app/shared/deposit-certificate-dialog/deposit-certificate-dialog.component.html new file mode 100644 index 000000000..e2862f74f --- /dev/null +++ b/src/app/shared/deposit-certificate-dialog/deposit-certificate-dialog.component.html @@ -0,0 +1,160 @@ + +

+ {{ accountType === 'FD' ? 'Fixed Deposit' : 'Recurring Deposit' }} Certificate + +

+ +
+ +
+
+
+ +
+ {{ accountType === 'FD' ? 'savings' : 'event_repeat' }} +
+ +
+
Nexora Co-operative Bank Ltd.
+
Mumbai, Maharashtra, India
+
+ {{ accountType === 'FD' ? 'FIXED DEPOSIT CERTIFICATE' : 'RECURRING DEPOSIT CERTIFICATE' }} +
+
Issued under the Common Seal of the Bank
+
+ +
+
+ Certificate No. + {{ certificateNo }} +
+
+ Account No. + {{ accountData.accountNo }} +
+
+ Date of Issue + {{ issueDate | dateFormat }} +
+
+ +
+

+ This is to certify that + {{ accountData.clientName || accountData.groupName }} + holds a {{ accountType === 'FD' ? 'Fixed Deposit' : 'Recurring Deposit' }} account bearing account number + {{ accountData.accountNo }} + with Nexora Co-operative Bank Ltd., as per the particulars set out + below. +

+ +
+
ISSUED TO:
+
{{ accountData.clientName || accountData.groupName }}
+
+ Customer ID: {{ accountData.clientAccountNo }} + + · PAN: {{ panNumber }} +
+
+ {{ clientAddress }} +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Product{{ accountData.depositProductName }}{{ accountType === 'FD' ? 'Deposit Date' : 'First Deposit Date' }}{{ depositDate | dateFormat }}
Deposit Amount + {{ accountData.depositAmount | currency: accountData.currency.code : 'symbol-narrow' }} + Tenure{{ tenureLabel }}
Installment Amount + {{ + accountData.mandatoryRecommendedDepositAmount | currency: accountData.currency.code : 'symbol-narrow' + }} + Installment Frequency + Every {{ accountData.recurringFrequency }} {{ accountData.recurringFrequencyType?.value }} +
Tenure{{ tenureLabel }}Interest Rate (p.a.){{ accountData.nominalAnnualInterestRate }}%
Interest Rate (p.a.){{ accountData.nominalAnnualInterestRate }}%Interest Compounding{{ accountData.interestCompoundingPeriodType?.value }}
Maturity Date{{ accountData.maturityDate | dateFormat }}Current Balance + {{ accountData.summary?.accountBalance | currency: accountData.currency.code : 'symbol-narrow' }} +
MATURITY AMOUNT + {{ accountData.maturityAmount | currency: accountData.currency.code : 'symbol-narrow' }} +
+ + +
+
+
+
+ + + + + diff --git a/src/app/shared/deposit-certificate-dialog/deposit-certificate-dialog.component.scss b/src/app/shared/deposit-certificate-dialog/deposit-certificate-dialog.component.scss new file mode 100644 index 000000000..0db7334cb --- /dev/null +++ b/src/app/shared/deposit-certificate-dialog/deposit-certificate-dialog.component.scss @@ -0,0 +1,512 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +.dialog-header { + display: flex; + align-items: center; + border-bottom: 1px solid #e0e0e0; + padding-bottom: 12px; + margin-bottom: 16px; + font-weight: 500; + color: #1a252c; +} + +.dialog-content-container { + max-height: 80vh; + overflow-y: auto; + padding: 10px; +} + +/* Certificate Outer Container */ +.certificate-container { + background-color: #faf8f5; + padding: 20px; + font-family: Georgia, 'Times New Roman', Times, serif; + color: #2c2520; + box-shadow: 0 4px 20px rgb(0 0 0 / 8%); + border-radius: 8px; + max-width: 800px; + margin: 0 auto; +} + +/* Classic Ornate Border System */ +.certificate-border-outer { + border: 5px double #c5a059; /* Gold double border */ + padding: 8px; + background-color: #fdfcf7; /* Textured light cream */ + border-radius: 4px; + position: relative; +} + +.certificate-border-inner { + border: 1px solid #dcd1bd; + padding: 30px; + border-radius: 2px; + position: relative; + overflow: hidden; +} + +/* Watermark */ +.certificate-watermark { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + opacity: 0.03; + pointer-events: none; + z-index: 1; + + .watermark-icon { + font-size: 280px; + width: 280px; + height: 280px; + color: #c5a059; + } +} + +.certificate-header, +.cert-meta-row, +.cert-body-section, +.certificate-details-table, +.cert-footer { + position: relative; + z-index: 2; /* Ensure content is above watermark */ +} + +/* Header styling */ +.certificate-header { + text-align: center; + margin-bottom: 30px; + border-bottom: 2px solid #f2edd5; + padding-bottom: 20px; + + .bank-name { + font-size: 26px; + font-weight: bold; + color: #0b3c5d; /* Dark Premium Blue */ + text-transform: uppercase; + letter-spacing: 1.5px; + margin-bottom: 4px; + } + + .bank-sub { + font-size: 13px; + color: #7b6f63; + font-style: italic; + letter-spacing: 1px; + margin-bottom: 20px; + } + + .cert-title { + font-size: 28px; + font-weight: 400; + color: #c5a059; /* Classic Gold */ + letter-spacing: 4px; + margin-bottom: 5px; + font-family: Georgia, serif; + } + + .cert-subtitle { + font-size: 11px; + color: #7b6f63; + text-transform: uppercase; + letter-spacing: 2px; + } +} + +/* Meta Data Grid (Top of cert) */ +.cert-meta-row { + display: flex; + justify-content: space-between; + margin-bottom: 30px; + border-bottom: 1px solid #f2edd5; + padding-bottom: 15px; + + .meta-item { + display: flex; + flex-direction: column; + + .meta-label { + font-size: 11px; + text-transform: uppercase; + color: #928374; + letter-spacing: 1px; + margin-bottom: 3px; + } + + .meta-value { + font-size: 15px; + font-weight: bold; + color: #322f2b; + } + } +} + +/* Main Body text */ +.cert-body-section { + margin-bottom: 35px; + + .cert-main-text { + font-size: 15px; + line-height: 1.8; + text-align: center; + color: #4a453f; + margin-bottom: 25px; + + .highlight-text { + color: #1a202c; + font-weight: 600; + border-bottom: 1px dotted #c5a059; + padding-bottom: 1px; + } + } +} + +/* Issued To Details Block */ +.issued-to-details { + border: 1px solid #f0e6d2; + background-color: #fbfaf5; + padding: 15px 20px; + border-radius: 4px; + margin: 20px auto; + max-width: 90%; + text-align: left; + + .issued-to-title { + font-size: 10px; + font-weight: bold; + color: #c5a059; + letter-spacing: 1.5px; + margin-bottom: 6px; + } + + .client-name-details { + font-size: 18px; + font-weight: bold; + color: #0b3c5d; + margin-bottom: 4px; + } + + .client-ids { + font-size: 12px; + color: #666; + margin-bottom: 6px; + } + + .client-address { + font-size: 12px; + color: #7b6f63; + line-height: 1.4; + } +} + +/* Certificate Data Table */ +.certificate-details-table { + width: 100%; + border-collapse: collapse; + margin-bottom: 40px; + + td { + padding: 10px 12px; + border: 1px solid #ede8db; + font-size: 13px; + } + + .lbl-col { + background-color: #f7f5ee; + font-weight: bold; + color: #7b6f63; + width: 25%; + } + + .val-col { + color: #322f2b; + width: 25%; + } + + .highlight-row { + background-color: #f5f2e9; + border-top: 2px solid #c5a059; + } + + .lbl-col-highlight { + font-weight: bold; + color: #0b3c5d; + font-size: 14px; + letter-spacing: 1px; + text-align: right; + padding-right: 20px; + } + + .val-col-highlight { + font-size: 16px; + font-weight: bold; + color: #b8860b; /* Dark Goldenrod */ + } +} + +/* Certificate Footer Seal / Signatures */ +.cert-footer { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 20px; + + .seal-container { + width: 25%; + + .seal-ring { + width: 90px; + height: 90px; + border: 2px dashed #b8860b; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto; + transform: rotate(-15deg); + + .seal-text { + font-size: 10px; + font-weight: bold; + color: #b8860b; + text-align: center; + line-height: 1.2; + letter-spacing: 1px; + } + } + } + + .closing-text { + width: 45%; + font-size: 12px; + color: #7b6f63; + font-style: italic; + line-height: 1.5; + text-align: center; + } + + .signatures-container { + width: 30%; + text-align: center; + + .signature-block { + display: inline-block; + width: 80%; + + .signature-line { + border-bottom: 1px solid #7b6f63; + margin-bottom: 8px; + height: 40px; /* Space for signature */ + } + + .signatory-title { + font-size: 12px; + color: #7b6f63; + font-weight: bold; + text-transform: uppercase; + letter-spacing: 0.5px; + } + } + } +} + +/* Print CSS */ +@media print { + /* Everything below `::ng-deep` reaches outside this component's own template -- html/body and + the whole Angular Material CDK overlay chain never carry this component's scoping attribute + under the default emulated encapsulation, so a plain (scoped) selector would silently never + match any of them. `::ng-deep` is deprecated but still the correct tool for a component that + genuinely needs to override its host application's shell during print. */ + + /* The app shell pins html/body to a fixed viewport height with overflow hidden (its inner + panels handle their own scrolling on screen) -- left in place, that caps every printed page + at one viewport's worth of content and silently cuts off the rest instead of paginating. */ + ::ng-deep html, + ::ng-deep body { + height: auto !important; + overflow: visible !important; + } + + /* Hide non-overlay contents during print */ + ::ng-deep body > :not(.cdk-overlay-container) { + display: none !important; + } + + /* The dialog's own scroll region clips anything past 80vh -- printing only captures what a + scrollable/max-height box shows on screen, so both constraints must be lifted or the + certificate gets cut off after its first visible slice. */ + .dialog-content-container { + max-height: none !important; + overflow: visible !important; + } + + /* CDK's overlay wrapper/backdrop/pane are all `position: fixed` (needed to center the dialog + on screen) and the wrapper flex-centers its content -- during print, a fixed-position + ancestor is clipped to one page's height, so anything taller than that gets cut off and only + a thin cross-section (here, just the certificate's own border) survives. Every ancestor in + that chain has to become a normal, statically-flowing block for the full certificate to print. */ + ::ng-deep .cdk-overlay-backdrop { + display: none !important; + } + + ::ng-deep .cdk-overlay-container, + ::ng-deep .cdk-global-overlay-wrapper, + ::ng-deep .cdk-overlay-pane, + ::ng-deep .mat-mdc-dialog-container, + ::ng-deep .mdc-dialog__container, + ::ng-deep .mdc-dialog__surface { + position: static !important; + display: block !important; + max-height: none !important; + max-width: none !important; + height: auto !important; + width: auto !important; + overflow: visible !important; + transform: none !important; + } + + /* Make certificate fill page */ + #deposit-certificate-print { + position: static !important; + width: 100% !important; + height: auto !important; + padding: 0 !important; + margin: 0 !important; + background-color: #fff !important; + box-shadow: none !important; + } + + .certificate-border-outer { + border-width: 8px !important; + background-color: #fff !important; + } + + .no-print { + display: none !important; + } + + ::ng-deep .mat-mdc-dialog-container { + padding: 0 !important; + } + + /* Compact every spacing/font value so the whole certificate fits a single printed page -- + the on-screen sizing is generous by design, but that same height doesn't fit one page. */ + .certificate-container { + padding: 6px !important; + } + + .certificate-border-inner { + padding: 14px !important; + } + + .certificate-header { + margin-bottom: 10px !important; + padding-bottom: 10px !important; + + .bank-name { + font-size: 19px !important; + margin-bottom: 2px !important; + } + + .bank-sub { + font-size: 10px !important; + margin-bottom: 8px !important; + } + + .cert-title { + font-size: 19px !important; + letter-spacing: 2px !important; + margin-bottom: 2px !important; + } + + .cert-subtitle { + font-size: 9px !important; + } + } + + .cert-meta-row { + margin-bottom: 10px !important; + padding-bottom: 8px !important; + + .meta-label { + font-size: 9px !important; + margin-bottom: 1px !important; + } + + .meta-value { + font-size: 12px !important; + } + } + + .cert-body-section { + margin-bottom: 10px !important; + + .cert-main-text { + font-size: 11px !important; + line-height: 1.4 !important; + margin-bottom: 10px !important; + } + } + + .issued-to-details { + padding: 8px 14px !important; + margin: 8px auto !important; + + .client-name-details { + font-size: 14px !important; + } + + .client-ids, + .client-address { + font-size: 10px !important; + } + } + + .certificate-details-table { + margin-bottom: 12px !important; + + td { + padding: 5px 10px !important; + font-size: 10px !important; + } + + .lbl-col-highlight { + font-size: 12px !important; + } + + .val-col-highlight { + font-size: 13px !important; + } + } + + .cert-footer { + margin-top: 8px !important; + + .seal-ring { + width: 60px !important; + height: 60px !important; + } + + .seal-text { + font-size: 8px !important; + } + + .closing-text { + font-size: 10px !important; + } + + .signature-line { + height: 22px !important; + } + + .signatory-title { + font-size: 10px !important; + } + } +} diff --git a/src/app/shared/deposit-certificate-dialog/deposit-certificate-dialog.component.ts b/src/app/shared/deposit-certificate-dialog/deposit-certificate-dialog.component.ts new file mode 100644 index 000000000..66f924cd5 --- /dev/null +++ b/src/app/shared/deposit-certificate-dialog/deposit-certificate-dialog.component.ts @@ -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/. + */ + +import { Component, OnInit, inject } from '@angular/core'; +import { + MAT_DIALOG_DATA, + MatDialogRef, + MatDialogTitle, + MatDialogContent, + MatDialogActions, + MatDialogClose +} from '@angular/material/dialog'; +import { ClientsService } from 'app/clients/clients.service'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { DateFormatPipe } from '../../pipes/date-format.pipe'; +import { CurrencyPipe } from '@angular/common'; +import { MatButton } from '@angular/material/button'; +import { MatIcon } from '@angular/material/icon'; + +/** + * Printable deposit certificate for a Fixed Deposit or Recurring Deposit account -- handed to the + * customer as physical proof of their deposit. Shared between FD and RD since both account shapes + * carry the same core fields (depositAmount, nominalAnnualInterestRate, depositPeriod, maturityDate, + * maturityAmount); RD additionally shows its installment/frequency details. + */ +@Component({ + selector: 'mifosx-deposit-certificate-dialog', + templateUrl: './deposit-certificate-dialog.component.html', + styleUrls: ['./deposit-certificate-dialog.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatDialogTitle, + MatDialogContent, + MatDialogActions, + MatDialogClose, + DateFormatPipe, + CurrencyPipe, + MatButton, + MatIcon + ] +}) +export class DepositCertificateDialogComponent implements OnInit { + dialogRef = inject>(MatDialogRef); + data = inject(MAT_DIALOG_DATA); + clientsService = inject(ClientsService); + + /** 'FD' or 'RD' -- controls the title and which extra rows show. */ + accountType: 'FD' | 'RD'; + accountData: any; + + clientData: any; + clientAddress = ''; + panNumber = 'Not Available'; + + certificateNo = ''; + issueDate: Date = new Date(); + + ngOnInit() { + this.accountType = this.data.accountType; + this.accountData = this.data.accountData; + const clientId = this.data.clientId != null ? String(this.data.clientId) : null; + + this.certificateNo = `${this.accountType}-CERT-${this.accountData.accountNo}`; + + if (!clientId) { + return; + } + + this.clientsService.getClientData(clientId).subscribe({ + next: (client: any) => { + this.clientData = client; + }, + error: () => {} + }); + + this.clientsService.getClientAddressData(clientId).subscribe({ + next: (addresses: any[]) => { + if (addresses && addresses.length > 0) { + const permanent = addresses.find((a) => /permanent/i.test(a?.addressType || '')) || addresses[0]; + this.clientAddress = this.formatAddress(permanent); + } + }, + error: () => {} + }); + + this.clientsService.getClientIdentifiers(clientId).subscribe({ + next: (identifiers: any[]) => { + const idList = Array.isArray(identifiers) ? identifiers : (identifiers as any)?.pageItems || []; + const panId = idList.find((id: any) => /pan/i.test(id?.documentType?.name || '')); + if (panId) { + this.panNumber = panId.documentKey; + } + }, + error: () => {} + }); + } + + formatAddress(addr: any): string { + if (!addr) return ''; + return [ + addr.addressLine1, + addr.addressLine2, + addr.townVillage, + addr.city, + addr.stateName, + addr.countryName, + addr.postalCode + ] + .filter((p) => !!p && String(p).trim()) + .join(', '); + } + + get depositDate(): any { + return this.accountData?.timeline?.activatedOnDate ?? this.accountData?.timeline?.submittedOnDate ?? null; + } + + get tenureLabel(): string { + const period = this.accountData?.depositPeriod; + const freq = this.accountData?.depositPeriodFrequency?.value; + if (period == null) return 'N/A'; + return freq ? `${period} ${freq}` : String(period); + } + + printCertificate() { + window.print(); + } +}